diff --git a/Content.Client/GameObjects/Components/Chemistry/HyposprayComponent.cs b/Content.Client/GameObjects/Components/Chemistry/HyposprayComponent.cs new file mode 100644 index 0000000000..979d371e62 --- /dev/null +++ b/Content.Client/GameObjects/Components/Chemistry/HyposprayComponent.cs @@ -0,0 +1,68 @@ +using Content.Client.UserInterface.Stylesheets; +using Content.Client.Utility; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Timing; +using Robust.Shared.ViewVariables; + +#nullable enable + +namespace Content.Client.GameObjects.Components.Chemistry +{ + [RegisterComponent] + public sealed class HyposprayComponent : SharedHyposprayComponent, IItemStatus + { + [ViewVariables] private ReagentUnit CurrentVolume { get; set; } + [ViewVariables] private ReagentUnit TotalVolume { get; set; } + [ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded; + + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) + { + if (curState is not HyposprayComponentState cState) + return; + + CurrentVolume = cState.CurVolume; + TotalVolume = cState.MaxVolume; + _uiUpdateNeeded = true; + } + + Control IItemStatus.MakeControl() + { + return new StatusControl(this); + } + + private sealed class StatusControl : Control + { + private readonly HyposprayComponent _parent; + private readonly RichTextLabel _label; + + public StatusControl(HyposprayComponent parent) + { + _parent = parent; + _label = new RichTextLabel {StyleClasses = {StyleNano.StyleClassItemStatus}}; + AddChild(_label); + + parent._uiUpdateNeeded = true; + } + + protected override void Update(FrameEventArgs args) + { + base.Update(args); + if (!_parent._uiUpdateNeeded) + { + return; + } + + _parent._uiUpdateNeeded = false; + + _label.SetMarkup(Loc.GetString( + "Volume: [color=white]{0}/{1}[/color]", + _parent.CurrentVolume, _parent.TotalVolume)); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs b/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs new file mode 100644 index 0000000000..8b88d21d4b --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/HyposprayComponent.cs @@ -0,0 +1,141 @@ +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Mobs.State; +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.ComponentDependencies; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +#nullable enable + +namespace Content.Server.GameObjects.Components.Chemistry +{ + [RegisterComponent] + public sealed class HyposprayComponent : SharedHyposprayComponent, IAttack, ISolutionChange, IAfterInteract + { + [ViewVariables(VVAccess.ReadWrite)] public float ClumsyFailChance { get; set; } + [ViewVariables(VVAccess.ReadWrite)] public ReagentUnit TransferAmount { get; set; } + + [ComponentDependency] private readonly SolutionContainerComponent? _solution = default!; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, x => x.ClumsyFailChance, "ClumsyFailChance", 0.5f); + serializer.DataField(this, x => x.TransferAmount, "TransferAmount", ReagentUnit.New(5)); + } + + public override void Initialize() + { + base.Initialize(); + + Dirty(); + } + + bool IAttack.ClickAttack(AttackEventArgs eventArgs) + { + var target = eventArgs.TargetEntity; + var user = eventArgs.User; + + return TryDoInject(target, user); + } + + Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + TryDoInject(eventArgs.Target, eventArgs.User); + return Task.CompletedTask; + } + + private bool TryDoInject(IEntity? target, IEntity user) + { + if (target == null || !EligibleEntity(target)) + return false; + + var msgFormat = "You inject {0:TheName}."; + + if (target == user) + { + msgFormat = "You inject yourself."; + } + else if (EligibleEntity(user) && ClumsyComponent.TryRollClumsy(user, ClumsyFailChance)) + { + msgFormat = "Oops! You injected yourself!"; + target = user; + } + + if (_solution == null || _solution.CurrentVolume == 0) + { + user.PopupMessageCursor(Loc.GetString("It's empty!")); + return true; + } + + user.PopupMessage(Loc.GetString(msgFormat, target)); + if (target != user) + { + target.PopupMessage(Loc.GetString("You feel a tiny prick!")); + var meleeSys = EntitySystem.Get(); + var angle = new Angle(target.Transform.WorldPosition - user.Transform.WorldPosition); + meleeSys.SendLunge(angle, user); + } + + EntitySystem.Get().PlayFromEntity("/Audio/Items/hypospray.ogg", user); + + var targetSolution = target.GetComponent(); + + // Get transfer amount. May be smaller than _transferAmount if not enough room + var realTransferAmount = ReagentUnit.Min(TransferAmount, targetSolution.EmptyVolume); + + if (realTransferAmount <= 0) + { + user.PopupMessage(user, Loc.GetString("{0:TheName} is already full!", targetSolution.Owner)); + return true; + } + + // Move units from attackSolution to targetSolution + var removedSolution = _solution.SplitSolution(realTransferAmount); + + if (!targetSolution.CanAddSolution(removedSolution)) + { + return true; + } + + removedSolution.DoEntityReaction(target, ReactionMethod.Injection); + + targetSolution.TryAddSolution(removedSolution); + + static bool EligibleEntity(IEntity entity) + { + // TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag? + // In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else. + return entity.HasComponent() && entity.HasComponent(); + } + + return true; + } + + void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) + { + Dirty(); + } + + public override ComponentState GetComponentState() + { + if (_solution == null) + return new HyposprayComponentState(ReagentUnit.Zero, ReagentUnit.Zero); + + return new HyposprayComponentState(_solution.CurrentVolume, _solution.MaxVolume); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/SharedHyposprayComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SharedHyposprayComponent.cs new file mode 100644 index 0000000000..292e7ff36d --- /dev/null +++ b/Content.Shared/GameObjects/Components/Chemistry/SharedHyposprayComponent.cs @@ -0,0 +1,26 @@ +using System; +using Content.Shared.Chemistry; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Chemistry +{ + public abstract class SharedHyposprayComponent : Component + { + public sealed override string Name => "Hypospray"; + public sealed override uint? NetID => ContentNetIDs.HYPOSPRAY; + + [Serializable, NetSerializable] + protected sealed class HyposprayComponentState : ComponentState + { + public ReagentUnit CurVolume { get; } + public ReagentUnit MaxVolume { get; } + + public HyposprayComponentState(ReagentUnit curVolume, ReagentUnit maxVolume) : base(ContentNetIDs.HYPOSPRAY) + { + CurVolume = curVolume; + MaxVolume = maxVolume; + } + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index 0fe7b87c6c..4ce0dfa358 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -3,7 +3,8 @@ // Starting from 1000 to avoid crossover with engine. public static class ContentNetIDs { - // 1000 + // As a CMO main I hereby declare the hypospray worthy of ID #1000. + public const uint HYPOSPRAY = 1000; public const uint DESTRUCTIBLE = 1001; public const uint MAGAZINE_BARREL = 1002; public const uint HANDS = 1003; diff --git a/Resources/Audio/Items/hypospray.ogg b/Resources/Audio/Items/hypospray.ogg new file mode 100644 index 0000000000..92d73147a5 Binary files /dev/null and b/Resources/Audio/Items/hypospray.ogg differ diff --git a/Resources/Prototypes/Entities/Objects/hypospray.yml b/Resources/Prototypes/Entities/Objects/hypospray.yml new file mode 100644 index 0000000000..a2d11fbc7e --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/hypospray.yml @@ -0,0 +1,15 @@ +- type: entity + name: hypospray + parent: BaseItem + description: A sterile injector for rapid administration of drugs to patients. + id: Hypospray + components: + - type: Sprite + sprite: Objects/Specific/Medical/hypospray.rsi + state: hypo + - type: Item + sprite: Objects/Specific/Medical/hypospray.rsi + - type: SolutionContainer + maxVol: 30 + caps: AddTo, CanExamine + - type: Hypospray diff --git a/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/hypo.png b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/hypo.png new file mode 100644 index 0000000000..cab29e5b0a Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/hypo.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/inhand-left.png b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/inhand-left.png new file mode 100644 index 0000000000..e2007ee06a Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/inhand-right.png b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/inhand-right.png new file mode 100644 index 0000000000..e2007ee06a Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/meta.json new file mode 100644 index 0000000000..7f9678130e --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/hypospray.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 32, "y": 32}, "license": "Taken from https://github.com/tgstation/tgstation/tree/727eb0a445bccbdc2d472e158e96b87fc0e997a1", "copyright": "CC-BY-SA-3.0", "states": [{"name": "hypo", "directions": 1, "delays": [[1.0]]}, {"name": "inhand-left", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "inhand-right", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}