Skip to content

Commit

Permalink
More Leash Tweaks (#414) (#126)
Browse files Browse the repository at this point in the history
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

adds PR #414 from Floof Station
Fansana/floofstation1#414

- Leashes now have configurable length. You can choose the preferred
length in the verb menu.
- The short leash proto has been removed and an appropriate prototype
migration has been added.
- Leashes were made anchorable (so we can finally keep Seb safe)
- Leash sprites were visually scaled down (70% the original size)
- Shock and bell collars have been reparented to ClothingNeckCollarBase.
- Leash anchors now a visual offset field, that properly incorporates
the visual rotation and scaling of the entity.
- Leashes now use a brand new overlay renderer (a refactored
JointVisualsOverlay)
- Fixed a bug where the "remove leash" verb would not appear on the
respective clothing.
  • Loading branch information
sleepyyapril authored Jan 5, 2025
2 parents 194b82c + 469c0ee commit 287ae62
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 77 deletions.
89 changes: 89 additions & 0 deletions Content.Client/Floofstation/Leash/LeashVisualsOverlay.cs
Original file line number Diff line number Diff line change
@@ -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<TransformComponent> _xformQuery;
private readonly EntityQuery<SpriteComponent> _spriteQuery;

public LeashVisualsOverlay(IEntityManager entMan)
{
_entMan = entMan;
_sprites = _entMan.System<SpriteSystem>();
_xform = _entMan.System<SharedTransformSystem>();
_xformQuery = _entMan.GetEntityQuery<TransformComponent>();
_spriteQuery = _entMan.GetEntityQuery<SpriteComponent>();
}

protected override void Draw(in OverlayDrawArgs args)
{
var worldHandle = args.WorldHandle;
worldHandle.SetTransform(Vector2.Zero, Angle.Zero);

var query = _entMan.EntityQueryEnumerator<LeashedVisualsComponent>();
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);
}
}
}
21 changes: 21 additions & 0 deletions Content.Client/Floofstation/Leash/LeashVisualsSystem.cs
Original file line number Diff line number Diff line change
@@ -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<LeashVisualsOverlay>();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Numerics;

namespace Content.Shared.Floofstation.Leash.Components;

/// <summary>
Expand All @@ -6,4 +8,9 @@ namespace Content.Shared.Floofstation.Leash.Components;
[RegisterComponent]
public sealed partial class LeashAnchorComponent : Component
{
/// <summary>
/// The visual offset of the "anchor point".
/// </summary>
[DataField]
public Vector2 Offset = Vector2.Zero;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public sealed partial class LeashComponent : Component
[DataField, AutoNetworkedField]
public float Length = 3.5f;

/// <summary>
/// List of possible lengths this leash may be assigned to be the user. If null, the length cannot be changed.
/// </summary>
[DataField, AutoNetworkedField]
public float[]? LengthConfigs;

/// <summary>
/// Maximum distance between the anchor and the puller beyond which the leash will break.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Numerics;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;

namespace Content.Shared.Floofstation.Leash.Components;

/// <summary>
/// Draws a line between this entity and the target. Same as JointVisualsComponent.
/// </summary>
[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;
}
114 changes: 83 additions & 31 deletions Content.Shared/Floofstation/Leash/LeashSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -47,6 +52,7 @@ public override void Initialize()

SubscribeLocalEvent<LeashComponent, EntGotInsertedIntoContainerMessage>(OnLeashInserted);
SubscribeLocalEvent<LeashComponent, EntGotRemovedFromContainerMessage>(OnLeashRemoved);
SubscribeLocalEvent<LeashComponent, GetVerbsEvent<AlternativeVerb>>(OnGetLeashVerbs);

SubscribeLocalEvent<LeashAnchorComponent, LeashAttachDoAfterEvent>(OnAttachDoAfter);
SubscribeLocalEvent<LeashedComponent, LeashDetachDoAfterEvent>(OnDetachDoAfter);
Expand All @@ -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<JointComponent>(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<JointComponent>(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<LeashAnchorComponent> ent, ref BeingUnequippedAttemptEvent args)
Expand All @@ -108,7 +127,7 @@ private void OnAnchorUnequipping(Entity<LeashAnchorComponent> ent, ref BeingUneq
if (TryGetLeashTarget(args.Equipment, out var leashTarget)
&& TryComp<LeashedComponent>(leashTarget, out var leashed)
&& leashed.Puller is not null
)
)
args.Cancel();
}

Expand All @@ -130,13 +149,14 @@ private void OnGetEquipmentVerbs(Entity<LeashAnchorComponent> 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<LeashedComponent>(leashTarget, out var leashedComp)
|| leashedComp.Puller != leash
|| HasComp<LeashedComponent>(leashTarget)) // This one means that OnGetLeashedVerbs will add a verb to remove it
|| HasComp<LeashedComponent>(ent)) // This one means that OnGetLeashedVerbs will add a verb to remove it
return;

var unleashVerb = new EquipmentVerb
Expand All @@ -163,6 +183,23 @@ private void OnGetLeashedVerbs(Entity<LeashedComponent> ent, ref GetVerbsEvent<I
});
}

private void OnGetLeashVerbs(Entity<LeashComponent> ent, ref GetVerbsEvent<AlternativeVerb> 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<LeashedComponent> ent, ref JointRemovedEvent args)
{
var id = args.Joint.ID;
Expand Down Expand Up @@ -301,11 +338,15 @@ public bool CanCreateJoint(EntityUid a, EntityUid b)
private DistanceJoint CreateLeashJoint(string jointId, Entity<LeashComponent> 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;
Expand Down Expand Up @@ -423,9 +464,11 @@ public void DoLeash(Entity<LeashAnchorComponent> anchor, Entity<LeashComponent>
_container.EnsureContainer<ContainerSlot>(leashTarget, LeashedComponent.VisualsContainerName);
if (EntityManager.TrySpawnInContainer(null, leashTarget, LeashedComponent.VisualsContainerName, out var visualEntity))
{
var visualComp = EnsureComp<JointVisualsComponent>(visualEntity.Value);
var visualComp = EnsureComp<LeashedVisualsComponent>(visualEntity.Value);
visualComp.Sprite = sprite;
visualComp.Target = leash;
visualComp.Source = leash;
visualComp.Target = leashTarget;
visualComp.OffsetTarget = anchor.Comp.Offset;

data.LeashVisuals = GetNetEntity(visualEntity);
}
Expand Down Expand Up @@ -459,6 +502,15 @@ public void RemoveLeash(Entity<LeashedComponent?> leashed, Entity<LeashComponent
Dirty(leash);
}

/// <summary>
/// Sets the desired length of the leash. The actual length will be updated on the next physics tick.
/// </summary>
public void SetLeashLength(Entity<LeashComponent> leash, float length)
{
leash.Comp.Length = length;
RefreshJoints(leash);
}

/// <summary>
/// Refreshes all joints for the specified leash.
/// This will remove all obsolete joints, such as those for which CanCreateJoint returns false,
Expand Down
1 change: 1 addition & 0 deletions Resources/Locale/en-US/Floof/verbs/verb-categories.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
verb-categories-leash-config = Set Length
1 change: 1 addition & 0 deletions Resources/Locale/en-US/floofstation/leash/leash-verbs.ftl
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions Resources/Migrations/floofmigration.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions Resources/Prototypes/Entities/Clothing/Neck/misc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -93,4 +93,3 @@
- type: EmitsSoundOnMove
soundCollection:
collection: FootstepJester
- type: LeashAnchor # Floofstation, silly bell collar can be leashed :3
2 changes: 0 additions & 2 deletions Resources/Prototypes/Entities/Structures/Machines/lathe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@
- ClothingHeadHatWelding
- ShockCollar # FloofStation
- LeashBasic # FloofStation
- ShortLeash # FloofStation
- type: EmagLatheRecipes
emagStaticRecipes:
- BoxLethalshot
Expand Down Expand Up @@ -1255,7 +1254,6 @@
- ShockCollar
- CustomDrinkJug
- LeashBasic
- ShortLeash
- CellRechargerCircuitboard
- WeaponCapacitorRechargerCircuitboard
- Beaker
Expand Down
Loading

0 comments on commit 287ae62

Please sign in to comment.