diff --git a/Content.Client/Physics/JointVisualsOverlay.cs b/Content.Client/Physics/JointVisualsOverlay.cs index 09c02746e2e..5c61338aaba 100644 --- a/Content.Client/Physics/JointVisualsOverlay.cs +++ b/Content.Client/Physics/JointVisualsOverlay.cs @@ -1,7 +1,9 @@ +using System.Numerics; using Content.Shared.Physics; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Enums; +using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Dynamics.Joints; @@ -27,6 +29,8 @@ protected override void Draw(in OverlayDrawArgs args) { _drawn.Clear(); var worldHandle = args.WorldHandle; + // Floofstation: fix incorrect drawing box location due to incorrect coordinate system + worldHandle.SetTransform(Vector2.Zero, Angle.Zero); var spriteSystem = _entManager.System<SpriteSystem>(); var xformSystem = _entManager.System<SharedTransformSystem>(); diff --git a/Content.Server/FloofStation/Traits/Components/SquirtProducerComponent.cs b/Content.Server/FloofStation/Traits/Components/SquirtProducerComponent.cs new file mode 100644 index 00000000000..3bde1b7d8c4 --- /dev/null +++ b/Content.Server/FloofStation/Traits/Components/SquirtProducerComponent.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 SquirtProducerComponent : Component +{ + [DataField("solutionname")] + public string SolutionName = "vagina"; + + [DataField] + public ProtoId<ReagentPrototype> ReagentId = "NaturalLubricant"; + + [DataField] + public FixedPoint2 MaxVolume = FixedPoint2.New(25); + + [DataField] + public Entity<SolutionComponent>? Solution = null; + + [DataField] + public FixedPoint2 QuantityPerUpdate = 5; + + [DataField] + public float HungerUsage = 10f; + + [DataField] + public TimeSpan GrowthDelay = TimeSpan.FromSeconds(10); + + [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 index 6a5a927d417..d2f21480ca2 100644 --- a/Content.Server/FloofStation/Traits/LewdTraitSystem.cs +++ b/Content.Server/FloofStation/Traits/LewdTraitSystem.cs @@ -31,17 +31,17 @@ public override void Initialize() //Initializers SubscribeLocalEvent<CumProducerComponent, ComponentStartup>(OnComponentInitCum); SubscribeLocalEvent<MilkProducerComponent, ComponentStartup>(OnComponentInitMilk); - //SubscribeLocalEvent<SquirtProducerComponent, ComponentStartup>(OnComponentInitSquirt); //Unused-Trait is WIP + SubscribeLocalEvent<SquirtProducerComponent, ComponentStartup>(OnComponentInitSquirt); //Verbs SubscribeLocalEvent<CumProducerComponent, GetVerbsEvent<InnateVerb>>(AddCumVerb); SubscribeLocalEvent<MilkProducerComponent, GetVerbsEvent<InnateVerb>>(AddMilkVerb); - //SubscribeLocalEvent<SquirtProducerComponent, GetVerbsEvent<InnateVerb>>(AddSquirtVerb); //Unused-Trait is WIP + SubscribeLocalEvent<SquirtProducerComponent, GetVerbsEvent<InnateVerb>>(AddSquirtVerb); //Events SubscribeLocalEvent<CumProducerComponent, CummingDoAfterEvent>(OnDoAfterCum); SubscribeLocalEvent<MilkProducerComponent, MilkingDoAfterEvent>(OnDoAfterMilk); - //SubscribeLocalEvent<SquirtProducerComponent, SquirtingDoAfterEvent>(OnDoAfterSquirt); //Unused-Trait is WIP + SubscribeLocalEvent<SquirtProducerComponent, SquirtingDoAfterEvent>(OnDoAfterSquirt); } #region event handling @@ -61,13 +61,13 @@ private void OnComponentInitMilk(Entity<MilkProducerComponent> entity, ref Compo solutionMilk.AddReagent(entity.Comp.ReagentId, entity.Comp.MaxVolume - solutionMilk.Volume); } - //private void OnComponentInitSquirt(Entity<SquirtProducerComponent> entity, ref ComponentStartup args) //Unused-Trait is WIP - //{ - // var solutionSquirt = _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); - // solutionSquirt.MaxVolume = entity.Comp.MaxVolume; + private void OnComponentInitSquirt(Entity<SquirtProducerComponent> entity, ref ComponentStartup args) + { + var solutionSquirt = _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + solutionSquirt.MaxVolume = entity.Comp.MaxVolume; - // solutionSquirt.AddReagent(entity.Comp.ReagentId, entity.Comp.MaxVolume - solutionSquirt.Volume); - //} + solutionSquirt.AddReagent(entity.Comp.ReagentId, entity.Comp.MaxVolume - solutionSquirt.Volume); + } public void AddCumVerb(Entity<CumProducerComponent> entity, ref GetVerbsEvent<InnateVerb> args) { @@ -113,26 +113,26 @@ public void AddMilkVerb(Entity<MilkProducerComponent> entity, ref GetVerbsEvent< args.Verbs.Add(verbMilk); } - //public void AddSquirtVerb(Entity<SquirtProducerComponent> entity, ref GetVerbsEvent<InnateVerb> args) //Unused-Trait is WIP - //{ - // if (args.Using == null || - // !args.CanInteract || - // !EntityManager.HasComponent<RefillableSolutionComponent>(args.Using.Value)) //see if removing this part lets you milk on the ground. - // return; + public void AddSquirtVerb(Entity<SquirtProducerComponent> entity, ref GetVerbsEvent<InnateVerb> args) + { + if (args.Using == null || + !args.CanInteract || + !EntityManager.HasComponent<RefillableSolutionComponent>(args.Using.Value)) //see if removing this part lets you milk on the ground. + return; - // _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); + _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionName); - // var user = args.User; - // var used = args.Using.Value; + 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); - //} + 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<CumProducerComponent> entity, ref CummingDoAfterEvent args) { @@ -188,32 +188,32 @@ private void OnDoAfterMilk(Entity<MilkProducerComponent> entity, ref MilkingDoAf _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<SquirtProducerComponent> entity, ref SquirtingDoAfterEvent args) //Unused-Trait is WIP - //{ - // if (args.Cancelled || args.Handled || args.Args.Used == null) - // return; + private void OnDoAfterSquirt(Entity<SquirtProducerComponent> entity, ref SquirtingDoAfterEvent 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.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; + 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; - // } + 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; + 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); - //} + 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 @@ -249,27 +249,28 @@ private void AttemptMilk(Entity<MilkProducerComponent> lewd, EntityUid userUid, _doAfterSystem.TryStartDoAfter(doargs); } - //private void AttemptSquirt(Entity<SquirtProducerComponent> lewd, EntityUid userUid, EntityUid containerUid) //Unused-Trait is WIP - //{ - // if (!HasComp<SquirtProducerComponent>(userUid)) - // return; + private void AttemptSquirt(Entity<SquirtProducerComponent> lewd, EntityUid userUid, EntityUid containerUid) + { + if (!HasComp<SquirtProducerComponent>(userUid)) + return; - // var doargs = new DoAfterArgs(EntityManager, userUid, 5, new SquirtingDoAfterEvent(), lewd, lewd, used: containerUid) - // { - // BreakOnUserMove = true, - // BreakOnDamage = true, - // BreakOnTargetMove = true, - // MovementThreshold = 1.0f, - // }; + 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); - //} + _doAfterSystem.TryStartDoAfter(doargs); + } public override void Update(float frameTime) { base.Update(frameTime); - var queryCum = EntityQueryEnumerator<CumProducerComponent>(); //SquirtProducerComponent -unused , + var queryCum = EntityQueryEnumerator<CumProducerComponent>(); var queryMilk = EntityQueryEnumerator<MilkProducerComponent>(); + var querySquirt = EntityQueryEnumerator<SquirtProducerComponent>(); var now = _timing.CurTime; while (queryCum.MoveNext(out var uid, out var containerCum)) @@ -320,21 +321,29 @@ public override void Update(float frameTime) _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 _); - //} + while (querySquirt.MoveNext(out var uid, out var containerSquirt)) + { + if (now < containerSquirt.NextGrowth) + continue; + + containerSquirt.NextGrowth = now + containerSquirt.GrowthDelay; + + if (_mobState.IsDead(uid)) + continue; + + if (EntityManager.TryGetComponent(uid, out HungerComponent? hunger)) + { + if (_hunger.GetHungerThreshold(hunger) < HungerThreshold.Okay) + continue; + + //_hunger.ModifyHunger(uid, -containerMilk.HungerUsage, hunger); + } + + if (!_solutionContainer.ResolveSolution(uid, containerSquirt.SolutionName, ref containerSquirt.Solution)) + continue; + + _solutionContainer.TryAddReagent(containerSquirt.Solution.Value, containerSquirt.ReagentId, containerSquirt.QuantityPerUpdate, out _); + } } #endregion } diff --git a/Content.Shared/Floofstation/Leash/Components/LeashAnchorComponent.cs b/Content.Shared/Floofstation/Leash/Components/LeashAnchorComponent.cs new file mode 100644 index 00000000000..a9fdb555937 --- /dev/null +++ b/Content.Shared/Floofstation/Leash/Components/LeashAnchorComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Floofstation.Leash.Components; + +/// <summary> +/// Indicates that this entity or the entity that wears this entity can be leashed. +/// </summary> +[RegisterComponent] +public sealed partial class LeashAnchorComponent : Component +{ +} diff --git a/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs b/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs new file mode 100644 index 00000000000..20ba744c0a4 --- /dev/null +++ b/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs @@ -0,0 +1,103 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Floofstation.Leash.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LeashComponent : Component +{ + /// <summary> + /// Maximum number of leash joints that this entity can create. + /// </summary> + [DataField, AutoNetworkedField] + public int MaxJoints = 1; + + /// <summary> + /// Default length of the leash joint. + /// </summary> + [DataField, AutoNetworkedField] + public float Length = 3.5f; + + /// <summary> + /// Maximum distance between the anchor and the puller beyond which the leash will break. + /// </summary> + [DataField, AutoNetworkedField] + public float MaxDistance = 8f; + + /// <summary> + /// The time it takes for one entity to attach/detach the leash to/from another entity. + /// </summary> + [DataField, AutoNetworkedField] + public TimeSpan AttachDelay = TimeSpan.FromSeconds(2f), DetachDelay = TimeSpan.FromSeconds(2f); + + /// <summary> + /// The time it takes for the leashed entity to detach itself from this leash. + /// </summary> + [DataField, AutoNetworkedField] + public TimeSpan SelfDetachDelay = TimeSpan.FromSeconds(8f); + + [DataField, AutoNetworkedField] + public SpriteSpecifier? LeashSprite; + + [DataField] + public TimeSpan NextPull = TimeSpan.Zero; + + [DataField, AutoNetworkedField] + public TimeSpan PullInterval = TimeSpan.FromSeconds(1.5f); + + /// <summary> + /// How much damage each leash joint can sustain before it breaks. + /// </summary> + /// <remarks>Not currently implemented; needs to be reworked in order to work.</remarks> + [DataField, AutoNetworkedField] + public float BreakDamage = 20f; + + /// <summary> + /// How much damage each leash joint loses every <see cref="DamageInterval"/>. + /// </summary> + /// <remarks>Not currently implemented; needs to be reworked in order to work.</remarks> + [DataField, AutoNetworkedField] + public float JointRepairDamage = 1f; + + /// <summary> + /// Interval at which damage is calculated for each joint. + /// </summary> + /// <remarks>Not currently implemented; needs to be reworked in order to work.</remarks> + [DataField, AutoNetworkedField] + public TimeSpan DamageInterval = TimeSpan.FromMilliseconds(200); + + /// <summary> + /// List of all joints and their respective pulled entities created by this leash. + /// </summary> + [DataField, AutoNetworkedField] + public List<LeashData> Leashed = new(); + + [DataDefinition, Serializable, NetSerializable] + public sealed partial class LeashData + { + [DataField] + public string JointId = string.Empty; + + [DataField] + public NetEntity Pulled = NetEntity.Invalid; + + /// <summary> + /// Entity used to visualize the leash. Created dynamically. + /// </summary> + [DataField] + public NetEntity? LeashVisuals = null; + + [DataField] + public float Damage = 0f; + + [DataField] + public TimeSpan NextDamage = TimeSpan.Zero; + + public LeashData(string jointId, NetEntity pulled) + { + JointId = jointId; + Pulled = pulled; + } + }; +} diff --git a/Content.Shared/Floofstation/Leash/Components/LeashedComponent.cs b/Content.Shared/Floofstation/Leash/Components/LeashedComponent.cs new file mode 100644 index 00000000000..616b33404cd --- /dev/null +++ b/Content.Shared/Floofstation/Leash/Components/LeashedComponent.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.Floofstation.Leash.Components; + +[RegisterComponent] +public sealed partial class LeashedComponent : Component +{ + public const string VisualsContainerName = "leashed-visuals"; + + [DataField] + public string? JointId = null; + + [NonSerialized] + public EntityUid? Puller = null, Anchor = null; +} diff --git a/Content.Shared/Floofstation/Leash/LeashDoAfterEvents.cs b/Content.Shared/Floofstation/Leash/LeashDoAfterEvents.cs new file mode 100644 index 00000000000..2b27055aa90 --- /dev/null +++ b/Content.Shared/Floofstation/Leash/LeashDoAfterEvents.cs @@ -0,0 +1,14 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Floofstation.Leash; + +[Serializable, NetSerializable] +public sealed partial class LeashAttachDoAfterEvent : SimpleDoAfterEvent +{ +} + +[Serializable, NetSerializable] +public sealed partial class LeashDetachDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Floofstation/Leash/LeashSystem.cs b/Content.Shared/Floofstation/Leash/LeashSystem.cs new file mode 100644 index 00000000000..fcd77a5994a --- /dev/null +++ b/Content.Shared/Floofstation/Leash/LeashSystem.cs @@ -0,0 +1,430 @@ +using System.Linq; +using Content.Shared.Clothing.Components; +using Content.Shared.DoAfter; +using Content.Shared.Floofstation.Leash.Components; +using Content.Shared.Hands.Components; +using Content.Shared.Input; +using Content.Shared.Inventory.Events; +using Content.Shared.Movement.Pulling.Events; +using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Physics; +using Content.Shared.Popups; +using Content.Shared.Throwing; +using Content.Shared.Verbs; +using Robust.Shared.Containers; +using Robust.Shared.Input.Binding; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Dynamics.Joints; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Player; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Floofstation.Leash; + +public sealed class LeashSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfters = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedJointSystem _joints = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly ThrowingSystem _throwing = default!; + [Dependency] private readonly SharedTransformSystem _xform = default!; + + public override void Initialize() + { + UpdatesBefore.Add(typeof(SharedPhysicsSystem)); + + SubscribeLocalEvent<LeashAnchorComponent, BeingUnequippedAttemptEvent>(OnAnchorUnequipping); + SubscribeLocalEvent<LeashedComponent, ContainerGettingInsertedAttemptEvent>(OnLeashedInserting); + SubscribeLocalEvent<LeashComponent, JointRemovedEvent>(OnJointRemoved); + SubscribeLocalEvent<LeashAnchorComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs); + SubscribeLocalEvent<LeashedComponent, GetVerbsEvent<InteractionVerb>>(OnGetLeashedVerbs); + + SubscribeLocalEvent<LeashAnchorComponent, LeashAttachDoAfterEvent>(OnAttachDoAfter); + SubscribeLocalEvent<LeashedComponent, LeashDetachDoAfterEvent>(OnDetachDoAfter); + + CommandBinds.Builder + .BindBefore(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestPullLeash), before: [typeof(PullingSystem)]) + .Register<LeashSystem>(); + } + + public override void Shutdown() + { + base.Shutdown(); + CommandBinds.Unregister<LeashSystem>(); + } + + public override void Update(float frameTime) + { + var leashQuery = EntityQueryEnumerator<LeashComponent, PhysicsComponent>(); + + while (leashQuery.MoveNext(out var leashEnt, out var leash, out var physics)) + { + var sourceXForm = Transform(leashEnt); + + foreach (var data in leash.Leashed.ToList()) + { + if (data.Pulled == NetEntity.Invalid || !TryGetEntity(data.Pulled, out var target)) + continue; + + // Break each leash joint whose entities are on different maps or are too far apart + var targetXForm = Transform(target.Value); + if (targetXForm.MapUid != sourceXForm.MapUid + || !sourceXForm.Coordinates.TryDistance(EntityManager, targetXForm.Coordinates, out var dst) + || dst > leash.MaxDistance) + RemoveLeash(target.Value, (leashEnt, leash)); + + // Calculate joint damage + if (_timing.CurTime < data.NextDamage + || !TryComp<JointComponent>(target, out var jointComp) + || !jointComp.GetJoints.TryGetValue(data.JointId, out var joint)) + continue; + + // TODO reaction force always returns 0 and thus damage doesn't work + // TODO find another way to calculate how much force is being excerted to hold the two entities together + // var damage = joint.GetReactionForce(1 / (float) leash.DamageInterval.TotalSeconds).Length() - leash.JointRepairDamage; + // data.Damage = Math.Max(0f, data.Damage + damage); + // data.NextDamage = _timing.CurTime + leash.DamageInterval; + // + // if (damage >= leash.BreakDamage && !_net.IsClient) + // { + // _popups.PopupPredicted(Loc.GetString("leash-snap-popup", ("leash", leashEnt)), target, null, PopupType.SmallCaution); + // RemoveLeash(target, (leashEnt, leash), true); + // } + } + } + + leashQuery.Dispose(); + } + + #region event handling + + private void OnAnchorUnequipping(Entity<LeashAnchorComponent> ent, ref BeingUnequippedAttemptEvent args) + { + // Prevent unequipping the anchor clothing until the leash is removed + if (TryGetLeashTarget(args.Equipment, out var leashTarget) + && TryComp<LeashedComponent>(leashTarget, out var leashed) + && leashed.Puller is not null + ) + args.Cancel(); + } + + private void OnLeashedInserting(Entity<LeashedComponent> ent, ref ContainerGettingInsertedAttemptEvent args) + { + // Prevent the entity from entering crates and the like because that would instantly break all joints on it, including the leash + if (!Exists(ent.Comp.Puller) + || !Exists(ent.Comp.Anchor) + || !TryComp<LeashComponent>(ent.Comp.Puller, out var leashPuller) + || !TryComp<LeashAnchorComponent>(ent.Comp.Anchor, out var leashAnchor)) + return; + + args.Cancel(); + // This is hella unsafe to do, but we recreate the joint because dumb storage system removes it before raising the event. + // We have to pray that OnJointRemoved already was called and that it deferred the removal of everything that used to exist + // I HATE STORAGE + DoLeash((ent.Comp.Anchor.Value, leashAnchor), (ent.Comp.Puller.Value, leashPuller), ent); + } + + private void OnJointRemoved(Entity<LeashComponent> ent, ref JointRemovedEvent args) + { + var id = args.Joint.ID; + if (!ent.Comp.Leashed.TryFirstOrDefault(it => it.JointId == id, out var data) + || !TryGetEntity(data.Pulled, out var leashedEnt) + || !TryComp<LeashedComponent>(leashedEnt, out var leashed)) + return; + + RemoveLeash((leashedEnt.Value, leashed), ent!, false); + } + + private void OnGetEquipmentVerbs(Entity<LeashAnchorComponent> ent, ref GetVerbsEvent<EquipmentVerb> args) + { + if (!args.CanAccess + || !args.CanInteract + || args.Using is not { } leash + || !TryComp<LeashComponent>(leash, out var leashComp)) + return; + + var user = args.User; + var leashVerb = new EquipmentVerb { Text = Loc.GetString("verb-leash-text") }; + + if (CanLeash(ent, (leash, leashComp))) + leashVerb.Act = () => TryLeash(ent, (leash, leashComp), user); + else + { + leashVerb.Message = Loc.GetString("verb-leash-error-message"); + leashVerb.Disabled = true; + } + args.Verbs.Add(leashVerb); + + + if (!TryGetLeashTarget(ent!, out var leashTarget) + || !TryComp<LeashedComponent>(leashTarget, out var leashedComp) + || leashedComp.Puller != leash + || HasComp<LeashedComponent>(leashTarget)) // This one means that OnGetLeashedVerbs will add a verb to remove it + return; + + var unleashVerb = new EquipmentVerb + { + Text = Loc.GetString("verb-unleash-text"), + Act = () => TryUnleash((leashTarget, leashedComp), (leash, leashComp), user) + }; + args.Verbs.Add(unleashVerb); + } + + private void OnGetLeashedVerbs(Entity<LeashedComponent> ent, ref GetVerbsEvent<InteractionVerb> args) + { + if (!args.CanAccess + || !args.CanInteract + || ent.Comp.Puller is not { } leash + || !TryComp<LeashComponent>(leash, out var leashComp)) + return; + + var user = args.User; + args.Verbs.Add(new InteractionVerb + { + Text = Loc.GetString("verb-unleash-text"), + Act = () => TryUnleash(ent!, (leash, leashComp), user) + }); + } + + private void OnAttachDoAfter(Entity<LeashAnchorComponent> ent, ref LeashAttachDoAfterEvent args) + { + if (args.Cancelled || args.Handled + || !TryComp<LeashComponent>(args.Used, out var leash) + || !CanLeash(ent, (args.Used.Value, leash))) + return; + + DoLeash(ent, (args.Used.Value, leash), EntityUid.Invalid); + } + + private void OnDetachDoAfter(Entity<LeashedComponent> ent, ref LeashDetachDoAfterEvent args) + { + if (args.Cancelled || args.Handled || ent.Comp.Puller is not { } leash) + return; + + RemoveLeash(ent!, leash); + } + + private bool OnRequestPullLeash(ICommonSession? session, EntityCoordinates targetCoords, EntityUid uid) + { + if (session?.AttachedEntity is not { } player + || !player.IsValid() + || !TryComp<HandsComponent>(player, out var hands) + || hands.ActiveHandEntity is not {} leash + || !TryComp<LeashComponent>(leash, out var leashComp) + || leashComp.NextPull > _timing.CurTime) + return false; + + // find the entity closest to the target coords + var candidates = leashComp.Leashed + .Select(it => GetEntity(it.Pulled)) + .Where(it => it != EntityUid.Invalid) + .Select(it => (it, Transform(it).Coordinates.TryDistance(EntityManager, _xform, targetCoords, out var dist) ? dist : float.PositiveInfinity)) + .Where(it => it.Item2 < float.PositiveInfinity) + .ToList(); + + if (candidates.Count == 0) + return false; + + // And pull it towards the user + var pulled = candidates.MinBy(it => it.Item2).Item1; + var playerCoords = Transform(player).Coordinates; + var pulledCoords = Transform(pulled).Coordinates; + var pullDir = _xform.ToMapCoordinates(playerCoords).Position - _xform.ToMapCoordinates(pulledCoords).Position; + + _throwing.TryThrow(pulled, pullDir * 0.5f, user: player, pushbackRatio: 1f, strength: 3f, animated: false, recoil: false, playSound: false, doSpin: false); + + leashComp.NextPull = _timing.CurTime + leashComp.PullInterval; + return true; + } + + #endregion + + #region private api + + /// <summary> + /// Tries to find the entity that gets leashed for the given anchor entity. + /// </summary> + private bool TryGetLeashTarget(Entity<LeashAnchorComponent?> ent, out EntityUid leashTarget) + { + leashTarget = default; + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + if (TryComp<ClothingComponent>(ent, out var clothing)) + { + if (clothing.InSlot == null || !_container.TryGetContainingContainer(ent, out var container)) + return false; + + leashTarget = container.Owner; + return true; + } + + leashTarget = ent.Owner; + return true; + } + + private DistanceJoint CreateLeashJoint(string jointId, Entity<LeashComponent> leash, EntityUid leashTarget) + { + var joint = _joints.CreateDistanceJoint(leash, leashTarget, id: jointId); + joint.CollideConnected = false; + joint.Length = leash.Comp.Length; + joint.MinLength = 0f; + joint.MaxLength = leash.Comp.Length; + joint.Stiffness = 1f; + joint.CollideConnected = true; // This is just for performance reasons and doesn't actually make mobs collide. + + return joint; + } + + #endregion + + #region public api + + public bool CanLeash(Entity<LeashAnchorComponent> anchor, Entity<LeashComponent> leash) + { + return leash.Comp.Leashed.Count < leash.Comp.MaxJoints + && TryGetLeashTarget(anchor!, out var leashTarget) + && CompOrNull<LeashedComponent>(leashTarget)?.JointId == null + && Transform(anchor).Coordinates.TryDistance(EntityManager, Transform(leash).Coordinates, out var dst) + && dst <= leash.Comp.Length + && !_xform.IsParentOf(Transform(leashTarget), leash); // google recursion - this makes the game explode for some reason + } + + public bool TryLeash(Entity<LeashAnchorComponent> anchor, Entity<LeashComponent> leash, EntityUid user, bool popup = true) + { + if (!CanLeash(anchor, leash) || !TryGetLeashTarget(anchor!, out var leashTarget)) + return false; + + // We reuse pulling attempt here because eugh it already exists + var attempt = new PullAttemptEvent(leash, anchor); + RaiseLocalEvent(anchor, attempt); + RaiseLocalEvent(leash, attempt); + + if (attempt.Cancelled) + return false; + + var doAfter = new DoAfterArgs(EntityManager, user, leash.Comp.AttachDelay, new LeashAttachDoAfterEvent(), anchor, leashTarget, leash) + { + BreakOnDamage = true, + BreakOnUserMove = true, + BreakOnTargetMove = true, + BreakOnWeightlessMove = true, + NeedHand = true + }; + + var result = _doAfters.TryStartDoAfter(doAfter); + if (result && _net.IsServer && popup) + { + (string, object)[] locArgs = [("user", user), ("target", leashTarget), ("anchor", anchor.Owner), ("selfAnchor", anchor.Owner == leashTarget)]; + + // This could've been much easier if my interaction verbs PR got merged already, but it isn't yet, so I gotta suffer + _popups.PopupEntity(Loc.GetString("leash-attaching-popup-self", locArgs), user, user); + if (user != leashTarget) + _popups.PopupEntity(Loc.GetString("leash-attaching-popup-target", locArgs), leashTarget, leashTarget); + + var othersFilter = Filter.PvsExcept(leashTarget).RemovePlayerByAttachedEntity(user); + _popups.PopupEntity(Loc.GetString("leash-attaching-popup-others", locArgs), leashTarget, othersFilter, true); + } + return result; + } + + public bool TryUnleash(Entity<LeashedComponent?> leashed, Entity<LeashComponent?> leash, EntityUid user, bool popup = true) + { + if (!Resolve(leashed, ref leashed.Comp, false) || !Resolve(leash, ref leash.Comp) || leashed.Comp.Puller != leash) + return false; + + var delay = user == leashed.Owner ? leash.Comp.SelfDetachDelay : leash.Comp.DetachDelay; + var doAfter = new DoAfterArgs(EntityManager, user, delay, new LeashDetachDoAfterEvent(), leashed.Owner, leashed) + { + BreakOnDamage = true, + BreakOnUserMove = true, + BreakOnTargetMove = true, + BreakOnWeightlessMove = true, + NeedHand = true + }; + + var result = _doAfters.TryStartDoAfter(doAfter); + if (result && _net.IsServer) + { + (string, object)[] locArgs = [("user", user), ("target", leashed.Owner), ("isSelf", user == leashed.Owner)]; + _popups.PopupEntity(Loc.GetString("leash-detaching-popup-self", locArgs), user, user); + _popups.PopupEntity(Loc.GetString("leash-detaching-popup-others", locArgs), user, Filter.PvsExcept(user), true); + } + + return result; + } + + /// <summary> + /// Immediately creates the leash joint between the specified entities and sets up respective components. + /// </summary> + /// <param name="anchor">The anchor entity, usually either target's clothing or the target itself.</param> + /// <param name="leash">The leash entity.</param> + /// <param name="leashTarget">The entity to which the leash is actually connected. Can be EntityUid.Invalid, then it will be deduced.</param> + public void DoLeash(Entity<LeashAnchorComponent> anchor, Entity<LeashComponent> leash, EntityUid leashTarget) + { + if (_net.IsClient || leashTarget is { Valid: false } && !TryGetLeashTarget(anchor!, out leashTarget)) + return; + + var leashedComp = EnsureComp<LeashedComponent>(leashTarget); + var netLeashTarget = GetNetEntity(leashTarget); + leashedComp.JointId = $"leash-joint-{netLeashTarget}"; + leashedComp.Puller = leash; + leashedComp.Anchor = anchor; + + // I'd like to use a chain joint or smth, but it's too hard and oftentimes buggy - lamia is a good bad example of that. + var joint = CreateLeashJoint(leashedComp.JointId, leash, leashTarget); + var data = new LeashComponent.LeashData(leashedComp.JointId, netLeashTarget) + { + NextDamage = _timing.CurTime + leash.Comp.DamageInterval + }; + + if (leash.Comp.LeashSprite is { } sprite) + { + _container.EnsureContainer<ContainerSlot>(leashTarget, LeashedComponent.VisualsContainerName); + if (EntityManager.TrySpawnInContainer(null, leashTarget, LeashedComponent.VisualsContainerName, out var visualEntity)) + { + var visualComp = EnsureComp<JointVisualsComponent>(visualEntity.Value); + visualComp.Sprite = sprite; + visualComp.Target = leash; + + data.LeashVisuals = GetNetEntity(visualEntity); + } + } + + leash.Comp.Leashed.Add(data); + Dirty(leash); + } + + public void RemoveLeash(Entity<LeashedComponent?> leashed, Entity<LeashComponent?> leash, bool breakJoint = true) + { + if (_net.IsClient || !Resolve(leashed, ref leashed.Comp)) + return; + + var jointId = leashed.Comp.JointId; + RemCompDeferred<LeashedComponent>(leashed); // Has to be deferred else the client explodes for some reason + + if (_container.TryGetContainer(leashed, LeashedComponent.VisualsContainerName, out var visualsContainer)) + _container.CleanContainer(visualsContainer); + + if (breakJoint && jointId is not null) + _joints.RemoveJoint(leash, jointId); + + if (Resolve(leash, ref leash.Comp, false)) + { + var leashedData = leash.Comp.Leashed.Where(it => it.JointId == jointId).ToList(); + foreach (var data in leashedData) + leash.Comp.Leashed.Remove(data); + } + + Dirty(leash); + } + + #endregion +} diff --git a/Content.Shared/Floofstation/Traits/Events/SquirtingDoAfterEvent.cs b/Content.Shared/Floofstation/Traits/Events/SquirtingDoAfterEvent.cs new file mode 100644 index 00000000000..2deb48b76af --- /dev/null +++ b/Content.Shared/Floofstation/Traits/Events/SquirtingDoAfterEvent.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 SquirtingDoAfterEvent : SimpleDoAfterEvent +{ +} + diff --git a/Resources/Changelog/Floof.yml b/Resources/Changelog/Floof.yml index cf0c7168b09..2198dabdcf4 100644 --- a/Resources/Changelog/Floof.yml +++ b/Resources/Changelog/Floof.yml @@ -612,3 +612,19 @@ Entries: message: 'Fixed defense values for arm-warmers ' id: 84 time: '2024-08-22T10:43:09.0000000+00:00' +- author: Mnemotechnician + changes: + - type: Add + message: >- + A new leash item has been added to autolathe to help you keep your + animals and station pets in check. If the station pet in question has + hands and can speak, the leash can be attached to their collar via strip + menu. + id: 85 + time: '2024-08-23T00:07:19.0000000+00:00' +- author: Memeji + changes: + - type: Add + message: '"Pussy" trait that creates a "Natural Lubricant"' + id: 86 + time: '2024-08-23T00:07:53.0000000+00:00' diff --git a/Resources/Locale/en-US/Floof/reagents/natural_sauce.ftl b/Resources/Locale/en-US/Floof/reagents/natural_sauce.ftl new file mode 100644 index 00000000000..797b6bcc2f8 --- /dev/null +++ b/Resources/Locale/en-US/Floof/reagents/natural_sauce.ftl @@ -0,0 +1,5 @@ +reagent-name-cum = cum +reagent-desc-cum = A sticky cloudy-white liquid. + +reagent-name-nat-lube = natural lubricant +reagent-desc-nat-lube = A slippery clear liquid. diff --git a/Resources/Locale/en-US/floofstation/cum/cum-verb.ftl b/Resources/Locale/en-US/Floof/verbs/cum-verb.ftl similarity index 100% rename from Resources/Locale/en-US/floofstation/cum/cum-verb.ftl rename to Resources/Locale/en-US/Floof/verbs/cum-verb.ftl diff --git a/Resources/Locale/en-US/floofstation/milk/milk-verb.ftl b/Resources/Locale/en-US/Floof/verbs/milk-verb.ftl similarity index 100% rename from Resources/Locale/en-US/floofstation/milk/milk-verb.ftl rename to Resources/Locale/en-US/Floof/verbs/milk-verb.ftl diff --git a/Resources/Locale/en-US/Floof/verbs/squirt-verb.ftl b/Resources/Locale/en-US/Floof/verbs/squirt-verb.ftl new file mode 100644 index 00000000000..3e10d9d7d3d --- /dev/null +++ b/Resources/Locale/en-US/Floof/verbs/squirt-verb.ftl @@ -0,0 +1,5 @@ +squirt-verb-dry = Your slit is dry. +squirt-verb-success = You fill {THE($target)} with {$amount}u of natural lubricant from your pussy. +squirt-verb-success-ground = You squirt out all over the ground! + +squirt-verb-get-text = Squirt diff --git a/Resources/Locale/en-US/floofstation/leash/leash-verbs.ftl b/Resources/Locale/en-US/floofstation/leash/leash-verbs.ftl new file mode 100644 index 00000000000..61d83139512 --- /dev/null +++ b/Resources/Locale/en-US/floofstation/leash/leash-verbs.ftl @@ -0,0 +1,3 @@ +verb-leash-text = Attach leash +verb-leash-error-message = Cannot attach the leash to this anchor. +verb-unleash-text = Detach leash diff --git a/Resources/Locale/en-US/floofstation/leash/leash.ftl b/Resources/Locale/en-US/floofstation/leash/leash.ftl new file mode 100644 index 00000000000..4d1a0a7f509 --- /dev/null +++ b/Resources/Locale/en-US/floofstation/leash/leash.ftl @@ -0,0 +1,20 @@ +leash-attaching-popup-self = You are trying to attach a leash to {$selfAnchor -> + [false] {THE($target)}'s {$anchor} + *[true] {THE($target)} +}... +leash-attaching-popup-target = {THE($target)} is trying to attach a leash to {$selfAnchor -> + [false] your {$anchor} + *[true] you +}... +leash-attaching-popup-others = {THE($user)} is trying to attach a leash to {$selfAnchor -> + [false] {THE($target)}'s {$anchor} + *[true] {THE($target)} +} + +leash-detaching-popup-self = You are trying to remove the leash... +leash-detaching-popup-others = {THE($user)} is trying to remove the leash {$isSelf -> + [true] from {REFLEXIVE($user)} + *[false] from {THE($target)} +}... + +leash-snap-popup = {THE($leash)} snaps off! diff --git a/Resources/Locale/en-US/reagents/floofstation/natural_sauce.ftl b/Resources/Locale/en-US/reagents/floofstation/natural_sauce.ftl deleted file mode 100644 index 887f9d8ba59..00000000000 --- a/Resources/Locale/en-US/reagents/floofstation/natural_sauce.ftl +++ /dev/null @@ -1,2 +0,0 @@ -reagent-name-cum = cum -reagent-desc-cum = A sticky cloudy-white liquid. diff --git a/Resources/Locale/en-US/supermatter/supermatter.ftl b/Resources/Locale/en-US/supermatter/supermatter.ftl index 52593f5524e..2f36560a26c 100644 --- a/Resources/Locale/en-US/supermatter/supermatter.ftl +++ b/Resources/Locale/en-US/supermatter/supermatter.ftl @@ -1,19 +1,19 @@ supermatter-announcer = Automatic Supermatter Engine supermatter-examine-integrity = Its' integrity is [color=yellow]{$integrity}%[/color]. -supermatter-announcement-warning = +supermatter-warning = Warning! Crystal hyperstructure integrity faltering! Integrity: {$integrity}%. -supermatter-announcement-emergency = +supermatter-emergency = DANGER! Crystal hyperstructure integrity reaching critical levels! Integrity: {$integrity}%. -supermatter-announcement-delam-explosion = +supermatter-delam-explosion = CRYSTAL DELAMINATION IMMINENT! The crystal has reached critical integrity failure! Emergency causality destabilization field has been engaged. -supermatter-announcement-delam-overmass = +supermatter-delam-overmass = CRYSTAL DELAMINATION IMMINENT! Crystal hyperstructure integrity has reached critical mass failure! Singularity formation imminent! -supermatter-announcement-delam-tesla = +supermatter-delam-tesla = CRYSTAL DELAMINATION IMMINENT! Crystal hyperstructure integrity has reached critical power surge failure! Energy ball formation imminent! -supermatter-announcement-delam-cascade = +supermatter-delam-cascade = CRYSTAL DELAMINATION IMMINENT! Harmonic frequency limits exceeded, casualty destabilization field could not be engaged! -supermatter-announcement-delam-cancel = +supermatter-delam-cancel = Crystalline hyperstructure returning to safe operating parameters. Failsafe has been Disengaged. Integrity: {$integrity}%. supermatter-seconds-before-delam = Estimated time before delamination: {$seconds} seconds. diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml index dd59d74d3f0..f7c70ada16e 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml @@ -71,6 +71,7 @@ - Fox understands: - Fox + - type: LeashAnchor # Floofstation - type: entity name: security dog @@ -185,3 +186,4 @@ understands: - Dog - GalacticCommon + - type: LeashAnchor # Floofstation diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 29234ea34cf..cfea8af020e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -254,6 +254,7 @@ factions: - Passive - type: RandomBark + - type: LeashAnchor # Floofstation - type: entity parent: MobChicken @@ -561,7 +562,7 @@ - type: FireVisuals sprite: Mobs/Effects/onfire.rsi normalState: Mouse_burning - + - type: LeashAnchor # Floofstation - moffroach my beloved # Note that the mallard duck is actually a male drake mallard, with the brown duck being the female variant of the same species, however ss14 lacks sex specific textures # The white duck is more akin to a pekin or call duck. @@ -642,6 +643,7 @@ - Passive - type: RandomBark barkMultiplier: 0.7 + - type: LeashAnchor # Floofstation - type: entity name: white duck #Quack @@ -829,6 +831,7 @@ - Moooooo - Moooo barkMultiplier: 3 + - type: LeashAnchor # Floofstation - type: entity @@ -997,6 +1000,7 @@ - type: HTN rootTask: task: RuminantHostileCompound + - type: LeashAnchor # Floofstation # Note that we gotta make this bitch vomit someday when you feed it anthrax or sumthin. Needs to be a small item thief too and aggressive if attacked. - type: entity @@ -1045,6 +1049,7 @@ - type: NpcFactionMember factions: - Passive + - type: LeashAnchor # Floofstation - type: entity name: gorilla @@ -1100,6 +1105,7 @@ - type: HTN rootTask: task: SimpleHostileCompound + - type: LeashAnchor # Floofstation - type: entity name: kangaroo @@ -1192,6 +1198,7 @@ - type: HTN rootTask: task: SimpleHostileCompound + - type: LeashAnchor # Floofstation - type: entity name: boxing kangaroo @@ -1313,6 +1320,7 @@ tags: - VimPilot - DoorBumpOpener + - type: LeashAnchor # Floofstation - type: entity name: monkey @@ -1822,6 +1830,7 @@ - type: Tag tags: - VimPilot + - type: LeashAnchor # Floofstation - type: entity @@ -1870,6 +1879,7 @@ interactSuccessSpawn: EffectHearts - type: Bloodstream bloodMaxVolume: 50 + - type: LeashAnchor # Floofstation - type: entity name: frog @@ -2446,6 +2456,7 @@ - Hissing understands: - Hissing + - type: LeashAnchor # Floofstation - type: entity @@ -2529,6 +2540,7 @@ - Hissing understands: - Hissing + - type: LeashAnchor # Floofstation - type: entity name: fox @@ -2617,6 +2629,7 @@ - Fox understands: - Fox + - type: LeashAnchor # Floofstation - type: entity name: corgi @@ -2686,6 +2699,7 @@ tags: - VimPilot - type: RandomBark + - type: LeashAnchor # Floofstation - type: entity name: corrupted corgi @@ -2847,6 +2861,7 @@ tags: - VimPilot - type: RandomBark + - type: LeashAnchor # Floofstation - type: entity name: calico cat @@ -3139,6 +3154,7 @@ - Hissing understands: - Hissing + - type: LeashAnchor # Floofstation - type: entity name: hamster @@ -3285,6 +3301,7 @@ - type: FireVisuals sprite: Mobs/Effects/onfire.rsi normalState: Mouse_burning + - type: LeashAnchor # Floofstation - type: entity name: pig diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 1daaedd368f..2ce77dd4f14 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -162,6 +162,7 @@ - HandheldStationMap - ClothingHeadHatWelding - CustomDrinkJug # FloofStation + - LeashBasic # FloofStation - type: EmagLatheRecipes emagStaticRecipes: - CartridgePistol diff --git a/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml b/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml index 5ce31b6f691..8696ee9ef9e 100644 --- a/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml +++ b/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml @@ -1,5 +1,13 @@ - type: entity parent: ClothingNeckBase + id: ClothingNeckCollarBase + abstract: true + components: + - type: Clothing + - type: LeashAnchor + +- type: entity + parent: ClothingNeckCollarBase id: ClothingNeckCollarBlue name: blue collar description: A cute blue collar. @@ -10,7 +18,7 @@ sprite: Floof/Clothing/Neck/collar_blue.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarBlack name: black collar description: A cute black collar. @@ -21,7 +29,7 @@ sprite: Floof/Clothing/Neck/collar_black.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarPink name: pink collar description: A cute pink collar. @@ -32,7 +40,7 @@ sprite: Floof/Clothing/Neck/collar_pink.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarEpi name: epistemics collar description: A collar showing how loyal you are to science! @@ -43,7 +51,7 @@ sprite: Floof/Clothing/Neck/collar_epi.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarEngi name: engineering collar description: A collar showing your deft skill utilizing your hands! @@ -54,7 +62,7 @@ sprite: Floof/Clothing/Neck/collar_engi.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarLogi name: logistics collar description: Who's a good courier? @@ -65,7 +73,7 @@ sprite: Floof/Clothing/Neck/collar_logi.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarMed name: medical collar description: Put your patients at ease by letting them know you're here to help. @@ -76,7 +84,7 @@ sprite: Floof/Clothing/Neck/collar_med.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarSec name: security collar description: Be the best guard dog on the station! @@ -87,7 +95,7 @@ sprite: Floof/Clothing/Neck/collar_sec.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarCmd name: Captain's collar description: The top dog (or cat) around. @@ -98,7 +106,7 @@ sprite: Floof/Clothing/Neck/collar_cmd.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarCC name: CentCom collar description: All bark with a whole lotta bite. @@ -109,7 +117,7 @@ sprite: Floof/Clothing/Neck/collar_cc.rsi - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase id: ClothingNeckCollarSyndi name: Blood-Red collar description: Sometimes you gotta be naughty. diff --git a/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml b/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml index 6fa6a2cfe3d..eae46ff0312 100644 --- a/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml +++ b/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml @@ -104,6 +104,7 @@ understands: - ScugSign - Cat + - type: LeashAnchor # Floofstation - type: palette id: ScugCatColors diff --git a/Resources/Prototypes/Floof/Entities/Objects/Tools/leash.yml b/Resources/Prototypes/Floof/Entities/Objects/Tools/leash.yml new file mode 100644 index 00000000000..5d09435d66f --- /dev/null +++ b/Resources/Prototypes/Floof/Entities/Objects/Tools/leash.yml @@ -0,0 +1,50 @@ +- type: entity + id: BaseLeash + parent: BaseItem + name: leash + description: Helps keep your animals close to you, as well as your friends. Attach to supported object or clothing (such as collars) to use. You can pull attached entities while holding the leash. + noSpawn: true + components: + - type: Sprite + sprite: Floof/Objects/Tools/leash.rsi + layers: + - state: icon + - type: Leash + leashSprite: + sprite: Floof/Objects/Tools/leash-rope.rsi + state: rope + +- type: entity + id: LeashBasic + parent: BaseLeash + components: + - type: Leash + length: 3.5 + attachDelay: 4.5 # Gotta be at least as high as cuffs or antags may abuse it + detachDelay: 3 + selfDetachDelay: 10 + +- type: entity + id: LeashAdvanced + parent: LeashBasic + name: advanced leash + components: + - type: Leash + maxJoints: 3 + attachDelay: 2.5 + detachDelay: 2 + selfDetachDelay: 15 + +- type: entity + id: LeashBluespace + parent: BaseLeash + name: bluespace leash + description: Powered by a miniature singularity inside the handle. Not safe for use by crewmembers. + suffix: DEBUG, DO NOT MAP + components: + - type: Leash + maxJoints: 25 + attachDelay: 0 + detachDelay: 10000 # will still be instant for admin ghosts or whatever with instant doafters tag + selfDetachDelay: 10000 + pullInterval: 0.1 diff --git a/Resources/Prototypes/Floof/Reagents/natural_sauce.yml b/Resources/Prototypes/Floof/Reagents/natural_sauce.yml index fe089b2ac80..3eed107e14f 100644 --- a/Resources/Prototypes/Floof/Reagents/natural_sauce.yml +++ b/Resources/Prototypes/Floof/Reagents/natural_sauce.yml @@ -19,3 +19,28 @@ collection: FootstepSticky params: volume: 6 + +- type: reagent + id: NaturalLubricant + name: reagent-name-nat-lube + group: NaturalSauce + desc: reagent-desc-nat-lube + slippery: true + physicalDesc: reagent-physical-desc-shiny + flavor: funny + color: "#d6d6d6" + recognizable: true + metabolisms: + Drink: + effects: + - !type:SatiateThirst + factor: 0.3 + footstepSound: + collection: FootstepSticky + params: + volume: 4 + tileReactions: + - !type:SpillTileReaction + paralyzeTime: 0.5 + launchForwardsMultiplier: 1.2 + requiredSlipSpeed: 1 diff --git a/Resources/Prototypes/Floof/Recipes/Lathes/tools.yml b/Resources/Prototypes/Floof/Recipes/Lathes/tools.yml new file mode 100644 index 00000000000..83e86c43658 --- /dev/null +++ b/Resources/Prototypes/Floof/Recipes/Lathes/tools.yml @@ -0,0 +1,9 @@ +- type: latheRecipe + id: LeashBasic + result: LeashBasic + completetime: 3.5 + materials: + Cloth: 50 + Plastic: 500 + Steel: 75 + diff --git a/Resources/Prototypes/Floof/Traits/lewd.yml b/Resources/Prototypes/Floof/Traits/lewd.yml index fb05c082657..cdcb36ad9a0 100644 --- a/Resources/Prototypes/Floof/Traits/lewd.yml +++ b/Resources/Prototypes/Floof/Traits/lewd.yml @@ -38,23 +38,22 @@ - ReagentId: Milk Quantity: 50 -# 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 +- type: trait + id: SquirtProducer + category: Physical + requirements: + - !type:CharacterJobRequirement + inverted: true + jobs: + - Borg + - MedicalBorg + components: + - type: SquirtProducer + solutionname: "vagina" + - type: SolutionContainerManager + solutions: + vagina: + maxVol: 25 + reagents: + - ReagentId: NaturalLubricant + Quantity: 25 diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml index 1266a721fe2..bfec2afcf06 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml @@ -22,3 +22,4 @@ - type: DeviceLinkSink ports: - Trigger + - type: LeashAnchor # floofstation diff --git a/Resources/Textures/Floof/Objects/Tools/leash-rope.rsi/meta.json b/Resources/Textures/Floof/Objects/Tools/leash-rope.rsi/meta.json new file mode 100644 index 00000000000..0efc53a9e93 --- /dev/null +++ b/Resources/Textures/Floof/Objects/Tools/leash-rope.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Mnemotechnician (github) for SS14", + "size": { + "x": 8, + "y": 64 + }, + "states": [ + { + "name": "rope" + } + ] +} diff --git a/Resources/Textures/Floof/Objects/Tools/leash-rope.rsi/rope.png b/Resources/Textures/Floof/Objects/Tools/leash-rope.rsi/rope.png new file mode 100644 index 00000000000..46bfe23e2ab Binary files /dev/null and b/Resources/Textures/Floof/Objects/Tools/leash-rope.rsi/rope.png differ diff --git a/Resources/Textures/Floof/Objects/Tools/leash.rsi/icon.png b/Resources/Textures/Floof/Objects/Tools/leash.rsi/icon.png new file mode 100644 index 00000000000..98caa62552c Binary files /dev/null and b/Resources/Textures/Floof/Objects/Tools/leash.rsi/icon.png differ diff --git a/Resources/Textures/Floof/Objects/Tools/leash.rsi/meta.json b/Resources/Textures/Floof/Objects/Tools/leash.rsi/meta.json new file mode 100644 index 00000000000..ade27d844e5 --- /dev/null +++ b/Resources/Textures/Floof/Objects/Tools/leash.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by Mnemotechnician (github) for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + } + ] +}