diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 627238f6e3..14b0c608fb 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -174,6 +174,8 @@ namespace Content.Client "Utensil", "UnarmedCombat", "TimedSpawner", + "Buckle", + "Strap" }; foreach (var ignoreName in registerIgnore) diff --git a/Content.Client/GameObjects/Components/Mobs/BuckleVisualizer2D.cs b/Content.Client/GameObjects/Components/Mobs/BuckleVisualizer2D.cs new file mode 100644 index 0000000000..3dfe3d10a4 --- /dev/null +++ b/Content.Client/GameObjects/Components/Mobs/BuckleVisualizer2D.cs @@ -0,0 +1,28 @@ +using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.Components.Strap; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Shared.Maths; + +namespace Content.Client.GameObjects.Components.Mobs +{ + [UsedImplicitly] + public class BuckleVisualizer2D : SpeciesVisualizer2D + { + public override void OnChangeData(AppearanceComponent component) + { + if (!component.TryGetData(SharedBuckleComponent.BuckleVisuals.Buckled, out var buckled) || + !buckled) + { + return; + } + + if (!component.TryGetData(SharedStrapComponent.StrapVisuals.RotationAngle, out var angle)) + { + return; + } + + SetRotation(component, Angle.FromDegrees(angle)); + } + } +} diff --git a/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs b/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs index 632c3a4377..fde442a473 100644 --- a/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/ClientStatusEffectsComponent.cs @@ -3,6 +3,7 @@ using System.Linq; using Content.Client.UserInterface; using Content.Client.Utility; using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.UserInterface; @@ -10,6 +11,7 @@ using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.GameObjects; +using Robust.Shared.Input; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Timing; @@ -94,31 +96,34 @@ namespace Content.Client.GameObjects.Components.Mobs _cooldown.Clear(); _ui.VBox.DisposeAllChildren(); - foreach (var (key, statusEffect) in _status.OrderBy(x => (int) x.Key)) + foreach (var (key, effect) in _status.OrderBy(x => (int) x.Key)) { - var status = new Control() - { - Children = - { - new TextureRect - { - TextureScale = (2, 2), - Texture = _resourceCache.GetTexture(statusEffect.Icon) - }, - } - }; + var texture = _resourceCache.GetTexture(effect.Icon); + var status = new StatusControl(key, texture); - if (statusEffect.Cooldown.HasValue) + if (effect.Cooldown.HasValue) { var cooldown = new CooldownGraphic(); status.Children.Add(cooldown); _cooldown[key] = cooldown; } + status.OnPressed += args => StatusPressed(args, status); + _ui.VBox.AddChild(status); } } + private void StatusPressed(BaseButton.ButtonEventArgs args, StatusControl status) + { + if (args.Event.Function != EngineKeyFunctions.UIClick) + { + return; + } + + SendNetworkMessage(new ClickStatusMessage(status.Effect)); + } + public void RemoveStatusEffect(StatusEffect name) { _status.Remove(name); diff --git a/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs b/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs index cfd7e09596..160b4cdf8e 100644 --- a/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs +++ b/Content.Client/GameObjects/Components/Mobs/SpeciesVisualizer2D.cs @@ -29,7 +29,7 @@ namespace Content.Client.GameObjects.Components.Mobs } } - private void SetRotation(AppearanceComponent component, Angle rotation) + protected void SetRotation(AppearanceComponent component, Angle rotation) { var sprite = component.Owner.GetComponent(); diff --git a/Content.Client/GameObjects/Components/Mobs/StatusControl.cs b/Content.Client/GameObjects/Components/Mobs/StatusControl.cs new file mode 100644 index 0000000000..591bf9e1c9 --- /dev/null +++ b/Content.Client/GameObjects/Components/Mobs/StatusControl.cs @@ -0,0 +1,25 @@ +using Content.Shared.GameObjects.Components.Mobs; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.GameObjects.Components.Mobs +{ + public class StatusControl : BaseButton + { + public readonly StatusEffect Effect; + + public StatusControl(StatusEffect effect, [CanBeNull] Texture texture) + { + Effect = effect; + + var item = new TextureRect + { + TextureScale = (2, 2), + Texture = texture + }; + + Children.Add(item); + } + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/BuckleComponent.cs b/Content.Server/GameObjects/Components/Mobs/BuckleComponent.cs new file mode 100644 index 0000000000..59a33a2132 --- /dev/null +++ b/Content.Server/GameObjects/Components/Mobs/BuckleComponent.cs @@ -0,0 +1,321 @@ +using Content.Server.GameObjects.Components.Strap; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Content.Server.Mobs; +using Content.Server.Utility; +using Content.Shared.GameObjects; +using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.Components.Strap; +using Content.Shared.GameObjects.EntitySystems; +using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Mobs +{ + [RegisterComponent] + public class BuckleComponent : SharedBuckleComponent, IActionBlocker, IInteractHand, IEffectBlocker + { +#pragma warning disable 649 + [Dependency] private readonly IEntitySystemManager _entitySystem; + [Dependency] private readonly IServerNotifyManager _notifyManager; +#pragma warning restore 649 + + private int _size; + + [ViewVariables, CanBeNull] + public StrapComponent BuckledTo { get; private set; } + + [ViewVariables] + public int Size => _size; + + private void BuckleStatus() + { + if (Owner.TryGetComponent(out ServerStatusEffectsComponent status)) + { + status.ChangeStatusEffectIcon(StatusEffect.Buckled, + BuckledTo == null + ? "/Textures/Mob/UI/Buckle/unbuckled.png" + : "/Textures/Mob/UI/Buckle/buckled.png"); + } + } + + private bool TryBuckle(IEntity user, IEntity to) + { + if (user == null || user == to) + { + return false; + } + + if (!ActionBlockerSystem.CanInteract(user)) + { + _notifyManager.PopupMessage(user, user, + Loc.GetString("You can't do that!")); + return false; + } + + var strapPosition = Owner.Transform.MapPosition; + var range = SharedInteractionSystem.InteractionRange / 2; + + if (!InteractionChecks.InRangeUnobstructed(user, strapPosition, range)) + { + _notifyManager.PopupMessage(user, user, + Loc.GetString("You can't reach there!")); + return false; + } + + if (!user.TryGetComponent(out HandsComponent hands)) + { + _notifyManager.PopupMessage(user, user, + Loc.GetString("You don't have hands!")); + return false; + } + + if (hands.GetActiveHand != null) + { + _notifyManager.PopupMessage(user, user, + Loc.GetString("Your hand isn't free!")); + return false; + } + + if (BuckledTo != null) + { + _notifyManager.PopupMessage(Owner, user, + Loc.GetString(Owner == user + ? "You are already buckled in!" + : "{0:They} are already buckled in!", Owner)); + return false; + } + + if (!to.TryGetComponent(out StrapComponent strap)) + { + _notifyManager.PopupMessage(Owner, user, + Loc.GetString(Owner == user + ? "You can't buckle yourself there!" + : "You can't buckle {0:them} there!", Owner)); + return false; + } + + var parent = to.Transform.Parent; + while (parent != null) + { + if (parent == user.Transform) + { + _notifyManager.PopupMessage(Owner, user, + Loc.GetString(Owner == user + ? "You can't buckle yourself there!" + : "You can't buckle {0:them} there!", Owner)); + return false; + } + + parent = parent.Parent; + } + + if (!strap.HasSpace(this)) + { + _notifyManager.PopupMessage(Owner, user, + Loc.GetString(Owner == user + ? "You can't fit there!" + : "{0:They} can't fit there!", Owner)); + return false; + } + + _entitySystem.GetEntitySystem() + .PlayFromEntity(strap.BuckleSound, Owner); + + if (!strap.TryAdd(this)) + { + _notifyManager.PopupMessage(Owner, user, + Loc.GetString(Owner == user + ? "You can't buckle yourself there!" + : "You can't buckle {0:them} there!", Owner)); + return false; + } + + BuckledTo = strap; + + if (Owner.TryGetComponent(out AppearanceComponent appearance)) + { + appearance.SetData(BuckleVisuals.Buckled, true); + } + + var ownTransform = Owner.Transform; + var strapTransform = strap.Owner.Transform; + + ownTransform.GridPosition = strapTransform.GridPosition; + ownTransform.AttachParent(strapTransform); + + switch (strap.Position) + { + case StrapPosition.None: + ownTransform.WorldRotation = strapTransform.WorldRotation; + break; + case StrapPosition.Stand: + StandingStateHelper.Standing(Owner); + ownTransform.WorldRotation = strapTransform.WorldRotation; + break; + case StrapPosition.Down: + StandingStateHelper.Down(Owner); + ownTransform.WorldRotation = Angle.South; + break; + } + + BuckleStatus(); + + return true; + } + + public bool TryUnbuckle(IEntity user, bool force = false) + { + if (BuckledTo == null) + { + return false; + } + + if (!force) + { + if (!ActionBlockerSystem.CanInteract(user)) + { + _notifyManager.PopupMessage(user, user, + Loc.GetString("You can't do that!")); + return false; + } + + var strapPosition = Owner.Transform.MapPosition; + var range = SharedInteractionSystem.InteractionRange / 2; + + if (!InteractionChecks.InRangeUnobstructed(user, strapPosition, range)) + { + _notifyManager.PopupMessage(user, user, + Loc.GetString("You can't reach there!")); + return false; + } + } + + if (BuckledTo.Owner.TryGetComponent(out StrapComponent strap)) + { + strap.Remove(this); + _entitySystem.GetEntitySystem() + .PlayFromEntity(strap.UnbuckleSound, Owner); + } + + Owner.Transform.DetachParent(); + Owner.Transform.WorldRotation = BuckledTo.Owner.Transform.WorldRotation; + BuckledTo = null; + + if (Owner.TryGetComponent(out AppearanceComponent appearance)) + { + appearance.SetData(BuckleVisuals.Buckled, false); + } + + if (Owner.TryGetComponent(out StunnableComponent stunnable) && stunnable.KnockedDown) + { + StandingStateHelper.Down(Owner); + } + else + { + StandingStateHelper.Standing(Owner); + } + + if (Owner.TryGetComponent(out SpeciesComponent species)) + { + species.CurrentDamageState.EnterState(Owner); + } + + BuckleStatus(); + + return true; + } + + public bool ToggleBuckle(IEntity user, IEntity to) + { + if (BuckledTo == null) + { + return TryBuckle(user, to); + } + else if (BuckledTo.Owner == to) + { + return TryUnbuckle(user); + } + else + { + return false; + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _size, "size", 100); + } + + protected override void Startup() + { + base.Startup(); + BuckleStatus(); + } + + public override void OnRemove() + { + base.OnRemove(); + + if (BuckledTo != null && BuckledTo.Owner.TryGetComponent(out StrapComponent strap)) + { + strap.Remove(this); + } + + BuckledTo = null; + BuckleStatus(); + } + + bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) + { + return TryUnbuckle(eventArgs.User); + } + + bool IActionBlocker.CanMove() + { + return BuckledTo == null; + } + + bool IActionBlocker.CanChangeDirection() + { + return BuckledTo == null; + } + + bool IEffectBlocker.CanFall() + { + return BuckledTo == null; + } + + [Verb] + private sealed class BuckleVerb : Verb + { + protected override void GetData(IEntity user, BuckleComponent component, VerbData data) + { + if (!ActionBlockerSystem.CanInteract(user) || + component.BuckledTo == null) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + data.Text = Loc.GetString("Unbuckle"); + } + + protected override void Activate(IEntity user, BuckleComponent component) + { + component.TryUnbuckle(user); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs b/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs index df70f1fee4..15acc85c78 100644 --- a/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using Content.Shared.GameObjects.Components.Mobs; +using JetBrains.Annotations; using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Players; namespace Content.Server.GameObjects.Components.Mobs { @@ -59,6 +62,44 @@ namespace Content.Server.GameObjects.Components.Mobs Dirty(); } + + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null) + { + base.HandleNetworkMessage(message, netChannel, session); + + if (session == null) + { + throw new ArgumentNullException(nameof(session)); + } + + switch (message) + { + case ClickStatusMessage msg: + { + var player = session.AttachedEntity; + + if (player != Owner) + { + break; + } + + // TODO: Implement clicking other status effects in the HUD + switch (msg.Effect) + { + case StatusEffect.Buckled: + if (!player.TryGetComponent(out BuckleComponent buckle)) + { + break; + } + + buckle.TryUnbuckle(player); + break; + } + + break; + } + } + } } } diff --git a/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs b/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs index e9930546ef..3098310a57 100644 --- a/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/StunnableComponent.cs @@ -100,7 +100,9 @@ namespace Content.Server.GameObjects.Components.Mobs seconds = MathF.Min(_knockdownTimer + (seconds * KnockdownTimeModifier), _knockdownCap); if (seconds <= 0f) + { return; + } StandingStateHelper.Down(Owner); diff --git a/Content.Server/GameObjects/Components/Strap/StrapComponent.cs b/Content.Server/GameObjects/Components/Strap/StrapComponent.cs new file mode 100644 index 0000000000..0391154cfd --- /dev/null +++ b/Content.Server/GameObjects/Components/Strap/StrapComponent.cs @@ -0,0 +1,215 @@ +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.GameObjects; +using Content.Shared.GameObjects.Components.Strap; +using Content.Shared.GameObjects.EntitySystems; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Strap +{ + [RegisterComponent] + public class StrapComponent : SharedStrapComponent, IInteractHand + { + private StrapPosition _position; + private string _buckleSound; + private string _unbuckleSound; + private int _rotation; + private int _size; + + /// + /// The entity that is currently buckled here, synced from + /// + private HashSet BuckledEntities { get; set; } + + /// + /// The change in position to the strapped mob + /// + public override StrapPosition Position + { + get => _position; + set + { + _position = value; + Dirty(); + } + } + + /// + /// The sound to be played when a mob is buckled + /// + [ViewVariables] + public string BuckleSound => _buckleSound; + + /// + /// The sound to be played when a mob is unbuckled + /// + [ViewVariables] + public string UnbuckleSound => _unbuckleSound; + + /// + /// The angle in degrees to rotate the player by when they get strapped + /// + [ViewVariables] + public int Rotation => _rotation; + + /// + /// The size of the strap which is compared against when buckling entities + /// + [ViewVariables] + public int Size => _size; + + /// + /// The sum of the sizes of all the buckled entities in this strap + /// + [ViewVariables] + public int OccupiedSize { get; private set; } + + public bool HasSpace(BuckleComponent buckle) + { + return OccupiedSize + buckle.Size <= _size; + } + + /// + /// Adds a buckled entity. Called from + /// + /// The component to add + /// Whether or not to check if the strap has enough space + /// True if added, false otherwise + public bool TryAdd(BuckleComponent buckle, bool force = false) + { + if (!force && !HasSpace(buckle)) + { + return false; + } + + if (!BuckledEntities.Add(buckle.Owner)) + { + return false; + } + + OccupiedSize += buckle.Size; + + if (buckle.Owner.TryGetComponent(out AppearanceComponent appearance)) + { + appearance.SetData(StrapVisuals.RotationAngle, _rotation); + } + + return true; + } + + /// + /// Removes a buckled entity. Called from + /// + /// The component to remove + public void Remove(BuckleComponent buckle) + { + if (BuckledEntities.Remove(buckle.Owner)) + { + OccupiedSize -= buckle.Size; + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _position, "position", StrapPosition.None); + serializer.DataField(ref _buckleSound, "buckleSound", "/Audio/effects/buckle.ogg"); + serializer.DataField(ref _unbuckleSound, "unbuckleSound", "/Audio/effects/unbuckle.ogg"); + serializer.DataField(ref _rotation, "rotation", 0); + + var defaultSize = 100; + + serializer.DataField(ref _size, "size", defaultSize); + BuckledEntities = new HashSet(_size / defaultSize); + + OccupiedSize = 0; + } + + public override void OnRemove() + { + base.OnRemove(); + + foreach (var entity in BuckledEntities) + { + if (entity.TryGetComponent(out BuckleComponent buckle)) + { + buckle.TryUnbuckle(entity, true); + } + } + + BuckledEntities.Clear(); + OccupiedSize = 0; + } + + [Verb] + private sealed class StrapVerb : Verb + { + protected override void GetData(IEntity user, StrapComponent component, VerbData data) + { + data.Visibility = VerbVisibility.Invisible; + + if (!ActionBlockerSystem.CanInteract(component.Owner) || + !user.TryGetComponent(out BuckleComponent buckle) || + buckle.BuckledTo != null && buckle.BuckledTo != component || + user == component.Owner) + { + return; + } + + var parent = component.Owner.Transform.Parent; + while (parent != null) + { + if (parent == user.Transform) + { + return; + } + + parent = parent.Parent; + } + + var userPosition = user.Transform.MapPosition; + var strapPosition = component.Owner.Transform.MapPosition; + var range = SharedInteractionSystem.InteractionRange / 2; + var inRange = EntitySystem.Get() + .InRangeUnobstructed(userPosition, strapPosition, range, + predicate: entity => entity == user || entity == component.Owner); + + if (!inRange) + { + return; + } + + data.Visibility = VerbVisibility.Visible; + data.Text = buckle.BuckledTo == null ? Loc.GetString("Buckle") : Loc.GetString("Unbuckle"); + } + + protected override void Activate(IEntity user, StrapComponent component) + { + if (!user.TryGetComponent(out BuckleComponent buckle)) + { + return; + } + + buckle.ToggleBuckle(user, component.Owner); + } + } + + bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out BuckleComponent buckle)) + { + return false; + } + + return buckle.ToggleBuckle(eventArgs.User, Owner); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/EffectBlockerSystem.cs b/Content.Server/GameObjects/EntitySystems/EffectBlockerSystem.cs new file mode 100644 index 0000000000..9468d2ddfb --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/EffectBlockerSystem.cs @@ -0,0 +1,32 @@ +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.GameObjects.EntitySystems +{ + /// + /// This interface gives components the ability to block certain effects + /// from affecting the owning entity. For actions see + /// + public interface IEffectBlocker + { + bool CanFall() => true; + } + + /// + /// Utility methods to check if an effect is allowed to affect a specific entity. + /// For actions see + /// + public class EffectBlockerSystem : EntitySystem + { + public static bool CanFall(IEntity entity) + { + var canFall = true; + foreach (var blocker in entity.GetAllComponents()) + { + canFall &= blocker.CanFall(); // Sets var to false if false + } + + return canFall; + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/HandsSystem.cs b/Content.Server/GameObjects/EntitySystems/HandsSystem.cs index 0c4cc5d9d7..a78c90043a 100644 --- a/Content.Server/GameObjects/EntitySystems/HandsSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/HandsSystem.cs @@ -7,13 +7,11 @@ using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.Input; using JetBrains.Annotations; using Robust.Server.GameObjects.EntitySystemMessages; -using Robust.Server.GameObjects.EntitySystems; using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Input; using Robust.Shared.Input.Binding; -using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; using Robust.Shared.IoC; using Robust.Shared.Localization; diff --git a/Content.Server/Mobs/StandingStateHelper.cs b/Content.Server/Mobs/StandingStateHelper.cs index b386990308..985dd1e448 100644 --- a/Content.Server/Mobs/StandingStateHelper.cs +++ b/Content.Server/Mobs/StandingStateHelper.cs @@ -1,14 +1,11 @@ +using Content.Server.GameObjects.EntitySystems; using Content.Server.Interfaces.GameObjects; using Content.Shared.Audio; using Content.Shared.GameObjects.Components.Mobs; using Robust.Server.GameObjects; using Robust.Server.GameObjects.EntitySystems; -using Robust.Shared.Audio; using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Interfaces.Random; using Robust.Shared.IoC; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; namespace Content.Server.Mobs { @@ -23,22 +20,30 @@ namespace Content.Server.Mobs /// False if the mob was already downed or couldn't set the state public static bool Down(IEntity entity, bool playSound = true, bool dropItems = true) { - if (!entity.TryGetComponent(out AppearanceComponent appearance)) return false; + if (!EffectBlockerSystem.CanFall(entity) || + !entity.TryGetComponent(out AppearanceComponent appearance)) + { + return false; + } + var newState = SharedSpeciesComponent.MobState.Down; appearance.TryGetData(SharedSpeciesComponent.MobVisuals.RotationState, out var oldState); - var newState = SharedSpeciesComponent.MobState.Down; - if (newState == oldState) - return false; - - appearance.SetData(SharedSpeciesComponent.MobVisuals.RotationState, newState); + if (newState != oldState) + { + appearance.SetData(SharedSpeciesComponent.MobVisuals.RotationState, newState); + } if (playSound) + { IoCManager.Resolve().GetEntitySystem() .PlayFromEntity(AudioHelpers.GetRandomFileFromSoundCollection("bodyfall"), entity, AudioHelpers.WithVariation(0.25f)); + } if(dropItems) + { DropAllItemsInHands(entity, false); + } return true; } diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedBuckleComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedBuckleComponent.cs new file mode 100644 index 0000000000..93ea7481b1 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Mobs/SharedBuckleComponent.cs @@ -0,0 +1,17 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Mobs +{ + public class SharedBuckleComponent : Component + { + public sealed override string Name => "Buckle"; + + [Serializable, NetSerializable] + public enum BuckleVisuals + { + Buckled + } + } +} diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedSpeciesComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedSpeciesComponent.cs index eed7165fbb..0f23bbda31 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedSpeciesComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedSpeciesComponent.cs @@ -27,6 +27,5 @@ namespace Content.Shared.GameObjects.Components.Mobs /// Down, } - } } diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs index 3fd9e677fd..6991ac8060 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs @@ -26,6 +26,21 @@ namespace Content.Shared.GameObjects.Components.Mobs } } + /// + /// A message that calls the click interaction on a status effect + /// + [Serializable, NetSerializable] + public class ClickStatusMessage : ComponentMessage + { + public readonly StatusEffect Effect; + + public ClickStatusMessage(StatusEffect effect) + { + Directed = true; + Effect = effect; + } + } + [Serializable, NetSerializable] public struct StatusEffectStatus { @@ -40,5 +55,6 @@ namespace Content.Shared.GameObjects.Components.Mobs Hunger, Thirst, Stun, + Buckled, } } diff --git a/Content.Shared/GameObjects/Components/Strap/SharedStrapComponent.cs b/Content.Shared/GameObjects/Components/Strap/SharedStrapComponent.cs new file mode 100644 index 0000000000..febdbbb3e8 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Strap/SharedStrapComponent.cs @@ -0,0 +1,37 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Strap +{ + public enum StrapPosition + { + /// + /// (Default) Makes no change to the buckled mob + /// + None = 0, + + /// + /// Makes the mob stand up + /// + Stand, + + /// + /// Makes the mob lie down + /// + Down + } + + public abstract class SharedStrapComponent : Component + { + public sealed override string Name => "Strap"; + + public virtual StrapPosition Position { get; set; } + + [Serializable, NetSerializable] + public enum StrapVisuals + { + RotationAngle + } + } +} diff --git a/Content.Shared/GameObjects/EntitySystems/ActionBlockerSystem.cs b/Content.Shared/GameObjects/EntitySystems/ActionBlockerSystem.cs index d22f5ebace..b5e1b50281 100644 --- a/Content.Shared/GameObjects/EntitySystems/ActionBlockerSystem.cs +++ b/Content.Shared/GameObjects/EntitySystems/ActionBlockerSystem.cs @@ -3,6 +3,10 @@ using Robust.Shared.Interfaces.GameObjects; namespace Content.Shared.GameObjects.EntitySystems { + /// + /// This interface gives components the ability to block certain actions from + /// being done by the owning entity. For effects see + /// public interface IActionBlocker { bool CanMove() => true; @@ -27,6 +31,10 @@ namespace Content.Shared.GameObjects.EntitySystems bool CanChangeDirection() => true; } + /// + /// Utility methods to check if a specific entity is allowed to perform an action. + /// For effects see + /// public class ActionBlockerSystem : EntitySystem { public static bool CanMove(IEntity entity) @@ -34,7 +42,7 @@ namespace Content.Shared.GameObjects.EntitySystems bool canmove = true; foreach(var actionblockercomponents in entity.GetAllComponents()) { - canmove &= actionblockercomponents.CanMove(); //sets var to false if false + canmove &= actionblockercomponents.CanMove(); // Sets var to false if false } return canmove; } diff --git a/Resources/Audio/effects/buckle.ogg b/Resources/Audio/effects/buckle.ogg new file mode 100644 index 0000000000..bb7dbe664b Binary files /dev/null and b/Resources/Audio/effects/buckle.ogg differ diff --git a/Resources/Audio/effects/unbuckle.ogg b/Resources/Audio/effects/unbuckle.ogg new file mode 100644 index 0000000000..a4a799f6df Binary files /dev/null and b/Resources/Audio/effects/unbuckle.ogg differ diff --git a/Resources/Prototypes/Entities/Buildings/furniture.yml b/Resources/Prototypes/Entities/Buildings/furniture.yml index 7ca4eba938..80619b957c 100644 --- a/Resources/Prototypes/Entities/Buildings/furniture.yml +++ b/Resources/Prototypes/Entities/Buildings/furniture.yml @@ -12,6 +12,8 @@ - type: Icon sprite: Buildings/furniture.rsi state: stool_base + - type: Strap + position: Stand - type: entity name: bar stool @@ -38,6 +40,8 @@ - type: Icon sprite: Buildings/furniture.rsi state: officechair_white + - type: Strap + position: Stand - type: entity name: dark office chair @@ -53,6 +57,8 @@ - type: Icon sprite: Buildings/furniture.rsi state: officechair_dark + - type: Strap + position: Stand - type: entity name: chair @@ -68,6 +74,8 @@ - type: Icon sprite: Buildings/furniture.rsi state: chair + - type: Strap + position: Stand - type: entity name: wooden chair @@ -84,11 +92,17 @@ name: bed id: Bed components: + - type: Clickable + - type: InteractionOutline + - type: Collidable - type: Sprite sprite: Buildings/furniture.rsi state: bed - type: Icon sprite: Buildings/furniture.rsi state: bed + - type: Strap + position: Down + rotation: -90 placement: mode: SnapgridCenter diff --git a/Resources/Prototypes/Entities/Buildings/shuttle.yml b/Resources/Prototypes/Entities/Buildings/shuttle.yml index 35590dbd45..6c21d6720b 100644 --- a/Resources/Prototypes/Entities/Buildings/shuttle.yml +++ b/Resources/Prototypes/Entities/Buildings/shuttle.yml @@ -10,19 +10,16 @@ - type: Icon sprite: Buildings/furniture.rsi state: chair - - type: Collidable - - type: Clickable - type: InteractionOutline - - type: EntityStorage showContents: true noDoor: true - - type: Damageable - type: Destructible thresholdvalue: 100 - - type: Physics - type: ShuttleController + - type: Strap + position: Stand diff --git a/Resources/Prototypes/Entities/Mobs/human.yml b/Resources/Prototypes/Entities/Mobs/human.yml index c12cd79d92..222da62c39 100644 --- a/Resources/Prototypes/Entities/Mobs/human.yml +++ b/Resources/Prototypes/Entities/Mobs/human.yml @@ -1,5 +1,5 @@ # Both humans and NPCs inherit from this. -# Anything player specific (e.g. UI, input) goes under HumanMob_Content +# Anything human specific (e.g. UI, input) goes under HumanMob_Content - type: entity save: false name: Urist McHands @@ -127,12 +127,14 @@ - type: Appearance visuals: - type: SpeciesVisualizer2D + - type: BuckleVisualizer2D - type: CombatMode - type: Teleportable - type: CharacterInfo - type: FootstepSound - type: HumanoidAppearance - type: AnimationPlayer + - type: Buckle - type: UnarmedCombat range: 0.8 arcwidth: 30 diff --git a/Resources/Textures/Mob/UI/Buckle/buckled.png b/Resources/Textures/Mob/UI/Buckle/buckled.png new file mode 100644 index 0000000000..2e1beea8e8 Binary files /dev/null and b/Resources/Textures/Mob/UI/Buckle/buckled.png differ diff --git a/Resources/Textures/Mob/UI/Buckle/unbuckled.png b/Resources/Textures/Mob/UI/Buckle/unbuckled.png new file mode 100644 index 0000000000..6879bd394f Binary files /dev/null and b/Resources/Textures/Mob/UI/Buckle/unbuckled.png differ