diff --git a/Content.Client/Floofstation/Leash/LeashVisualsOverlay.cs b/Content.Client/Floofstation/Leash/LeashVisualsOverlay.cs new file mode 100644 index 00000000000..41319ea29fb --- /dev/null +++ b/Content.Client/Floofstation/Leash/LeashVisualsOverlay.cs @@ -0,0 +1,89 @@ +using System.Numerics; +using Content.Shared.Floofstation.Leash.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client.Floofstation.Leash; + +public sealed class LeashVisualsOverlay : Overlay +{ + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + private readonly IEntityManager _entMan; + private readonly SpriteSystem _sprites; + private readonly SharedTransformSystem _xform; + private readonly EntityQuery _xformQuery; + private readonly EntityQuery _spriteQuery; + + public LeashVisualsOverlay(IEntityManager entMan) + { + _entMan = entMan; + _sprites = _entMan.System(); + _xform = _entMan.System(); + _xformQuery = _entMan.GetEntityQuery(); + _spriteQuery = _entMan.GetEntityQuery(); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var worldHandle = args.WorldHandle; + worldHandle.SetTransform(Vector2.Zero, Angle.Zero); + + var query = _entMan.EntityQueryEnumerator(); + while (query.MoveNext(out var visualsComp)) + { + if (visualsComp.Source is not {Valid: true} source + || visualsComp.Target is not {Valid: true} target + || !_xformQuery.TryGetComponent(source, out var xformComp) + || !_xformQuery.TryGetComponent(target, out var otherXformComp) + || xformComp.MapID != args.MapId + || otherXformComp.MapID != xformComp.MapID) + continue; + + var texture = _sprites.Frame0(visualsComp.Sprite); + var width = texture.Width / (float) EyeManager.PixelsPerMeter; + + var coordsA = xformComp.Coordinates; + var coordsB = otherXformComp.Coordinates; + + // If both coordinates are in the same spot (e.g. the leash is being held by the leashed), don't render anything + if (coordsA.TryDistance(_entMan, _xform, coordsB, out var dist) && dist < 0.01f) + continue; + + var rotA = xformComp.LocalRotation; + var rotB = otherXformComp.LocalRotation; + var offsetA = visualsComp.OffsetSource; + var offsetB = visualsComp.OffsetTarget; + + // Sprite rotation is the rotation along the Z axis + // Which is different from transform rotation for all mobs that are seen from the side (instead of top-down) + if (_spriteQuery.TryGetComponent(source, out var spriteA)) + { + offsetA *= spriteA.Scale; + rotA = spriteA.Rotation; + } + if (_spriteQuery.TryGetComponent(target, out var spriteB)) + { + offsetB *= spriteB.Scale; + rotB = spriteB.Rotation; + } + + coordsA = coordsA.Offset(rotA.RotateVec(offsetA)); + coordsB = coordsB.Offset(rotB.RotateVec(offsetB)); + + var posA = _xform.ToMapCoordinates(coordsA).Position; + var posB = _xform.ToMapCoordinates(coordsB).Position; + var diff = (posB - posA); + var length = diff.Length(); + + // So basically, we find the midpoint, then create a box that describes the sprite boundaries, then rotate it + var midPoint = diff / 2f + posA; + var angle = (posB - posA).ToWorldAngle(); + var box = new Box2(-width / 2f, -length / 2f, width / 2f, length / 2f); + var rotate = new Box2Rotated(box.Translated(midPoint), angle, midPoint); + + worldHandle.DrawTextureRect(texture, rotate); + } + } +} diff --git a/Content.Client/Floofstation/Leash/LeashVisualsSystem.cs b/Content.Client/Floofstation/Leash/LeashVisualsSystem.cs new file mode 100644 index 00000000000..05d0f6085e2 --- /dev/null +++ b/Content.Client/Floofstation/Leash/LeashVisualsSystem.cs @@ -0,0 +1,21 @@ +using Content.Client.Physics; +using Robust.Client.Graphics; + +namespace Content.Client.Floofstation.Leash; + +public sealed class LeashVisualsSystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + _overlay.AddOverlay(new LeashVisualsOverlay(EntityManager)); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlay.RemoveOverlay(); + } +} diff --git a/Content.Shared/Floofstation/Leash/Components/LeashAnchorComponent.cs b/Content.Shared/Floofstation/Leash/Components/LeashAnchorComponent.cs index a9fdb555937..465a96eb6e2 100644 --- a/Content.Shared/Floofstation/Leash/Components/LeashAnchorComponent.cs +++ b/Content.Shared/Floofstation/Leash/Components/LeashAnchorComponent.cs @@ -1,3 +1,5 @@ +using System.Numerics; + namespace Content.Shared.Floofstation.Leash.Components; /// @@ -6,4 +8,9 @@ namespace Content.Shared.Floofstation.Leash.Components; [RegisterComponent] public sealed partial class LeashAnchorComponent : Component { + /// + /// The visual offset of the "anchor point". + /// + [DataField] + public Vector2 Offset = Vector2.Zero; } diff --git a/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs b/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs index 181d61b0715..2e213a36ce1 100644 --- a/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs +++ b/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs @@ -19,6 +19,12 @@ public sealed partial class LeashComponent : Component [DataField, AutoNetworkedField] public float Length = 3.5f; + /// + /// List of possible lengths this leash may be assigned to be the user. If null, the length cannot be changed. + /// + [DataField, AutoNetworkedField] + public float[]? LengthConfigs; + /// /// Maximum distance between the anchor and the puller beyond which the leash will break. /// diff --git a/Content.Shared/Floofstation/Leash/Components/LeashedVisualsComponent.cs b/Content.Shared/Floofstation/Leash/Components/LeashedVisualsComponent.cs new file mode 100644 index 00000000000..2aeae642967 --- /dev/null +++ b/Content.Shared/Floofstation/Leash/Components/LeashedVisualsComponent.cs @@ -0,0 +1,21 @@ +using System.Numerics; +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Floofstation.Leash.Components; + +/// +/// Draws a line between this entity and the target. Same as JointVisualsComponent. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LeashedVisualsComponent : Component +{ + [DataField(required: true), AutoNetworkedField] + public SpriteSpecifier Sprite = default!; + + [DataField, AutoNetworkedField] + public EntityUid Source, Target; + + [DataField, AutoNetworkedField] + public Vector2 OffsetSource, OffsetTarget; +} diff --git a/Content.Shared/Floofstation/Leash/LeashSystem.cs b/Content.Shared/Floofstation/Leash/LeashSystem.cs index 9b4bd293f9a..411fee0116d 100644 --- a/Content.Shared/Floofstation/Leash/LeashSystem.cs +++ b/Content.Shared/Floofstation/Leash/LeashSystem.cs @@ -36,6 +36,11 @@ public sealed class LeashSystem : EntitySystem [Dependency] private readonly ThrowingSystem _throwing = default!; [Dependency] private readonly SharedTransformSystem _xform = default!; + public static VerbCategory LeashLengthConfigurationCategory = + new("verb-categories-leash-config", "/Textures/Floof/Interface/VerbIcons/resize.svg.192dpi.png"); + + #region Lifecycle + public override void Initialize() { UpdatesBefore.Add(typeof(SharedPhysicsSystem)); @@ -47,6 +52,7 @@ public override void Initialize() SubscribeLocalEvent(OnLeashInserted); SubscribeLocalEvent(OnLeashRemoved); + SubscribeLocalEvent>(OnGetLeashVerbs); SubscribeLocalEvent(OnAttachDoAfter); SubscribeLocalEvent(OnDetachDoAfter); @@ -69,37 +75,50 @@ public override void Update(float frameTime) 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; - - // Client side only: set max distance to infinity to prevent the client from ever predicting leashes. - if (_net.IsClient - && data.JointId is not null - && TryComp(target, out var jointComp) - && jointComp.GetJoints.TryGetValue(data.JointId, out var joint) - && joint is DistanceJoint distanceJoint - ) - distanceJoint.MaxLength = float.MaxValue; - - if (_net.IsClient) - 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)); - } + UpdateLeash(data, sourceXForm, leash, leashEnt); } leashQuery.Dispose(); } + private void UpdateLeash(LeashComponent.LeashData data, TransformComponent sourceXForm, LeashComponent leash, EntityUid leashEnt) + { + if (data.Pulled == NetEntity.Invalid || !TryGetEntity(data.Pulled, out var target)) + return; + + DistanceJoint? joint = null; + if (data.JointId is not null + && TryComp(target, out var jointComp) + && jointComp.GetJoints.TryGetValue(data.JointId, out var _joint) + ) + joint = _joint as DistanceJoint; + + // Client: set max distance to infinity to prevent the client from ever predicting leashes. + if (_net.IsClient) + { + if (joint is not null) + joint.MaxLength = float.MaxValue; + + return; + } + + // Server: 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)); + + // Server: update leash lengths if necessary/possible + // The length can be increased freely, but can only be decreased if the pulled entity is close enough + if (joint is not null && (leash.Length >= joint.MaxLength || leash.Length >= joint.Length)) + joint.MaxLength = leash.Length; + } + + #endregion + #region event handling private void OnAnchorUnequipping(Entity ent, ref BeingUnequippedAttemptEvent args) @@ -108,7 +127,7 @@ private void OnAnchorUnequipping(Entity ent, ref BeingUneq if (TryGetLeashTarget(args.Equipment, out var leashTarget) && TryComp(leashTarget, out var leashed) && leashed.Puller is not null - ) + ) args.Cancel(); } @@ -130,13 +149,14 @@ private void OnGetEquipmentVerbs(Entity ent, ref GetVerbsE leashVerb.Message = Loc.GetString("verb-leash-error-message"); leashVerb.Disabled = true; } + args.Verbs.Add(leashVerb); if (!TryGetLeashTarget(ent!, out var leashTarget) || !TryComp(leashTarget, out var leashedComp) || leashedComp.Puller != leash - || HasComp(leashTarget)) // This one means that OnGetLeashedVerbs will add a verb to remove it + || HasComp(ent)) // This one means that OnGetLeashedVerbs will add a verb to remove it return; var unleashVerb = new EquipmentVerb @@ -163,6 +183,23 @@ private void OnGetLeashedVerbs(Entity ent, ref GetVerbsEvent ent, ref GetVerbsEvent args) + { + if (ent.Comp.LengthConfigs is not { } configurations) + return; + + // Add a menu listing each length configuration + foreach (var length in configurations) + { + args.Verbs.Add(new AlternativeVerb + { + Text = Loc.GetString("verb-leash-set-length-text", ("length", length)), + Act = () => SetLeashLength(ent, length), + Category = LeashLengthConfigurationCategory + }); + } + } + private void OnJointRemoved(Entity ent, ref JointRemovedEvent args) { var id = args.Joint.ID; @@ -301,11 +338,15 @@ public bool CanCreateJoint(EntityUid a, EntityUid b) private DistanceJoint CreateLeashJoint(string jointId, Entity leash, EntityUid leashTarget) { var joint = _joints.CreateDistanceJoint(leash, leashTarget, id: jointId); + // If the soon-to-be-leashed entity is too far away, we don't force it any closer. + // The system will automatically reduce the length of the leash once it gets closer. + var length = Transform(leashTarget).Coordinates.TryDistance(EntityManager, Transform(leash).Coordinates, out var dist) + ? MathF.Max(dist, leash.Comp.Length) + : leash.Comp.Length; joint.CollideConnected = false; - joint.Length = leash.Comp.Length; joint.MinLength = 0f; - joint.MaxLength = leash.Comp.Length; + joint.MaxLength = length; joint.Stiffness = 1f; joint.CollideConnected = true; // This is just for performance reasons and doesn't actually make mobs collide. joint.Damping = 1f; @@ -425,9 +466,11 @@ public void DoLeash(Entity anchor, Entity _container.EnsureContainer(leashTarget, LeashedComponent.VisualsContainerName); if (EntityManager.TrySpawnInContainer(null, leashTarget, LeashedComponent.VisualsContainerName, out var visualEntity)) { - var visualComp = EnsureComp(visualEntity.Value); + var visualComp = EnsureComp(visualEntity.Value); visualComp.Sprite = sprite; - visualComp.Target = leash; + visualComp.Source = leash; + visualComp.Target = leashTarget; + visualComp.OffsetTarget = anchor.Comp.Offset; data.LeashVisuals = GetNetEntity(visualEntity); } @@ -461,6 +504,15 @@ public void RemoveLeash(Entity leashed, Entity + /// Sets the desired length of the leash. The actual length will be updated on the next physics tick. + /// + public void SetLeashLength(Entity leash, float length) + { + leash.Comp.Length = length; + RefreshJoints(leash); + } + /// /// Refreshes all joints for the specified leash. /// This will remove all obsolete joints, such as those for which CanCreateJoint returns false, diff --git a/Resources/Locale/en-US/Floof/leash/leash-verbs.ftl b/Resources/Locale/en-US/Floof/leash/leash-verbs.ftl index 61d83139512..6329d964529 100644 --- a/Resources/Locale/en-US/Floof/leash/leash-verbs.ftl +++ b/Resources/Locale/en-US/Floof/leash/leash-verbs.ftl @@ -1,3 +1,4 @@ verb-leash-text = Attach leash verb-leash-error-message = Cannot attach the leash to this anchor. verb-unleash-text = Detach leash +verb-leash-set-length-text = {$length} meters diff --git a/Resources/Locale/en-US/Floof/verbs/verb-categories.ftl b/Resources/Locale/en-US/Floof/verbs/verb-categories.ftl new file mode 100644 index 00000000000..ec591148c09 --- /dev/null +++ b/Resources/Locale/en-US/Floof/verbs/verb-categories.ftl @@ -0,0 +1 @@ +verb-categories-leash-config = Set Length diff --git a/Resources/Migrations/floofmigration.yml b/Resources/Migrations/floofmigration.yml index 5e4e0419c7c..c261f3a7670 100644 --- a/Resources/Migrations/floofmigration.yml +++ b/Resources/Migrations/floofmigration.yml @@ -1,3 +1,6 @@ # 2024-08-16 Floof Only Please message @FracturedSwords on discord if there are any merge conflicts upcoming SpawnMobArcticFoxSiobhan: SpawnMobArcticFoxSeb MobArcticFoxSiobhan: MobArcticFoxSeb + +# 2024-12-15: Leash now supports changing the length intrinsically +ShortLeash: LeashBasic diff --git a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml index 2581f06bb8f..03b17264276 100644 --- a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml @@ -81,7 +81,7 @@ priority: -1 - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase # Floof - reparented id: ClothingNeckBellCollar name: bell collar description: A way to inform others about your presence, or just to annoy everyone around you! @@ -93,4 +93,3 @@ - type: EmitsSoundOnMove soundCollection: collection: FootstepJester - - type: LeashAnchor # Floofstation, silly bell collar can be leashed :3 diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 3f87a32ec2b..fdd2b349dcb 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -198,7 +198,6 @@ - ClothingHeadHatWelding - ShockCollar # FloofStation - LeashBasic # FloofStation - - ShortLeash # FloofStation - type: EmagLatheRecipes emagStaticRecipes: - BoxLethalshot @@ -1232,7 +1231,6 @@ - ShockCollar - CustomDrinkJug - LeashBasic - - ShortLeash - CellRechargerCircuitboard - WeaponCapacitorRechargerCircuitboard - Beaker diff --git a/Resources/Prototypes/Floof/Catalog/Fills/Crates/lewd.yml b/Resources/Prototypes/Floof/Catalog/Fills/Crates/lewd.yml index 559626a760d..dd74e75a237 100644 --- a/Resources/Prototypes/Floof/Catalog/Fills/Crates/lewd.yml +++ b/Resources/Prototypes/Floof/Catalog/Fills/Crates/lewd.yml @@ -36,4 +36,4 @@ - id: DrinkCumBottleFull amount: 2 - id: LeashBasic - - id: ShortLeash + amount: 2 diff --git a/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml b/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml index 8696ee9ef9e..9215337badb 100644 --- a/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml +++ b/Resources/Prototypes/Floof/Entities/Clothing/Neck/collars.yml @@ -5,6 +5,7 @@ components: - type: Clothing - type: LeashAnchor + offset: 0, 0.22 - type: entity parent: ClothingNeckCollarBase diff --git a/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml b/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml index af3ee8c25e1..d1556f300ef 100644 --- a/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml +++ b/Resources/Prototypes/Floof/Entities/Mobs/NPCs/scugcat.yml @@ -105,6 +105,7 @@ - ScugSign - Cat - type: LeashAnchor # Floofstation + offset: 0, 0.2 - type: Vore - type: palette diff --git a/Resources/Prototypes/Floof/Entities/Objects/Tools/leash.yml b/Resources/Prototypes/Floof/Entities/Objects/Tools/leash.yml index 61609d781e2..072b8545c94 100644 --- a/Resources/Prototypes/Floof/Entities/Objects/Tools/leash.yml +++ b/Resources/Prototypes/Floof/Entities/Objects/Tools/leash.yml @@ -9,11 +9,15 @@ sprite: Floof/Objects/Tools/leash.rsi layers: - state: icon + scale: 0.7, 0.7 - type: Leash pullInterval: 0.75 leashSprite: sprite: Floof/Objects/Tools/leash-rope.rsi state: rope + - type: Anchorable + delay: 3 + snap: false # The first ever entity to set this field to false! However, this doesn't seem to work :c - type: entity id: LeashBasic @@ -21,30 +25,19 @@ components: - type: Leash length: 3 + lengthConfigs: [1.5, 2.25, 3] maxDistance: 6 attachDelay: 4 detachDelay: 4 selfDetachDelay: 10 -# TODO this should be named LeashShort... -- type: entity - id: ShortLeash - parent: BaseLeash - name: short leash - components: - - type: Leash - length: 1.5 - maxDistance: 3 - attachDelay: 4.5 - detachDelay: 3 - selfDetachDelay: 10 - - type: entity id: LeashAdvanced parent: LeashBasic name: advanced leash components: - type: Leash + lengthConfigs: [1, 1.5, 2.25, 3] maxJoints: 3 attachDelay: 2.5 detachDelay: 2 @@ -58,9 +51,10 @@ suffix: DEBUG, DO NOT MAP components: - type: Leash - maxDistance: 100 - maxJoints: 25 - attachDelay: 0 - detachDelay: 10000 # will still be instant for admin ghosts or whatever with instant doafters tag - selfDetachDelay: 10000 + lengthConfigs: [1, 1.5, 2.25, 3, 5, 10] + maxDistance: 20 + maxJoints: 10 + attachDelay: 2.5 + detachDelay: 5 + selfDetachDelay: 20 pullInterval: 0.1 diff --git a/Resources/Prototypes/Floof/Loadouts/items.yml b/Resources/Prototypes/Floof/Loadouts/items.yml index 0cec29c6be0..fa562950689 100644 --- a/Resources/Prototypes/Floof/Loadouts/items.yml +++ b/Resources/Prototypes/Floof/Loadouts/items.yml @@ -30,15 +30,3 @@ requirements: - !type:CharacterItemGroupRequirement group: LoadoutFun - -- type: loadout - id: LoadoutItemLeashShort - category: Items - cost: 2 - exclusive: true - items: - - ShortLeash - requirements: - - !type:CharacterItemGroupRequirement - group: LoadoutFun - diff --git a/Resources/Prototypes/Floof/Recipes/Lathes/tools.yml b/Resources/Prototypes/Floof/Recipes/Lathes/tools.yml index 69d3ecba2ac..2492871df64 100644 --- a/Resources/Prototypes/Floof/Recipes/Lathes/tools.yml +++ b/Resources/Prototypes/Floof/Recipes/Lathes/tools.yml @@ -6,13 +6,3 @@ Cloth: 50 Plastic: 500 Steel: 75 - -- type: latheRecipe - id: ShortLeash - result: ShortLeash - completetime: 2.5 - materials: - Cloth: 25 - Plastic: 300 - Steel: 50 - diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml index bfec2afcf06..d5979aa77c2 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingNeckBase + parent: ClothingNeckCollarBase # Floof - reparented id: ShockCollar name: shock collar description: Shocking. # DeltaV: sprite is fine @@ -22,4 +22,3 @@ - type: DeviceLinkSink ports: - Trigger - - type: LeashAnchor # floofstation diff --git a/Resources/Textures/Floof/Interface/VerbIcons/resize.svg b/Resources/Textures/Floof/Interface/VerbIcons/resize.svg new file mode 100644 index 00000000000..7f0b49ff7d9 --- /dev/null +++ b/Resources/Textures/Floof/Interface/VerbIcons/resize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Textures/Floof/Interface/VerbIcons/resize.svg.192dpi.png b/Resources/Textures/Floof/Interface/VerbIcons/resize.svg.192dpi.png new file mode 100644 index 00000000000..5947c728325 Binary files /dev/null and b/Resources/Textures/Floof/Interface/VerbIcons/resize.svg.192dpi.png differ