diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index d32aa57097..f3846fbae0 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -33,4 +33,7 @@
+
+
+
diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index cc0bb65e0c..0c98700378 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -74,7 +74,6 @@ namespace Content.Client
"Wirecutter",
"Screwdriver",
"Multitool",
- "Welder",
"Wrench",
"Crowbar",
"HitscanWeapon",
@@ -82,7 +81,6 @@ namespace Content.Client
"Projectile",
"MeleeWeapon",
"Storeable",
- "Stack",
"Dice",
"Construction",
"Apc",
@@ -90,12 +88,10 @@ namespace Content.Client
"PoweredLight",
"Smes",
"Powercell",
- "HandheldLight",
"LightBulb",
"Healing",
"Catwalk",
"BallisticMagazine",
- "BallisticMagazineWeapon",
"BallisticBullet",
"HitscanWeaponCapacitor",
"PowerCell",
diff --git a/Content.Client/GameObjects/Components/HandheldLightComponent.cs b/Content.Client/GameObjects/Components/HandheldLightComponent.cs
new file mode 100644
index 0000000000..b97f0717e7
--- /dev/null
+++ b/Content.Client/GameObjects/Components/HandheldLightComponent.cs
@@ -0,0 +1,106 @@
+using System;
+using Content.Shared.GameObjects.Components;
+using Robust.Client.Graphics.Drawing;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+using Robust.Shared.Timing;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.GameObjects.Components
+{
+ [RegisterComponent]
+ public sealed class HandheldLightComponent : SharedHandheldLightComponent, IItemStatus
+ {
+ [ViewVariables] public float? Charge { get; private set; }
+
+ public Control MakeControl()
+ {
+ return new StatusControl(this);
+ }
+
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ var cast = (HandheldLightComponentState) curState;
+
+ Charge = cast.Charge;
+ }
+
+ private sealed class StatusControl : Control
+ {
+ private const float TimerCycle = 1;
+
+ private readonly HandheldLightComponent _parent;
+ private readonly PanelContainer[] _sections = new PanelContainer[5];
+
+ private float _timer;
+
+ private static readonly StyleBoxFlat _styleBoxLit = new StyleBoxFlat
+ {
+ BackgroundColor = Color.Green
+ };
+
+ private static readonly StyleBoxFlat _styleBoxUnlit = new StyleBoxFlat
+ {
+ BackgroundColor = Color.Black
+ };
+
+ public StatusControl(HandheldLightComponent parent)
+ {
+ _parent = parent;
+
+ var wrapper = new HBoxContainer
+ {
+ SeparationOverride = 4,
+ SizeFlagsHorizontal = SizeFlags.ShrinkCenter
+ };
+
+ AddChild(wrapper);
+
+ for (var i = 0; i < _sections.Length; i++)
+ {
+ var panel = new PanelContainer {CustomMinimumSize = (20, 20)};
+ wrapper.AddChild(panel);
+ _sections[i] = panel;
+ }
+ }
+
+ protected override void Update(FrameEventArgs args)
+ {
+ base.Update(args);
+
+ _timer += args.DeltaSeconds;
+ _timer %= TimerCycle;
+
+ var charge = _parent.Charge ?? 0;
+
+ int level;
+
+ if (FloatMath.CloseTo(charge, 0))
+ {
+ level = 0;
+ }
+ else
+ {
+ level = 1 + (int) MathF.Round(charge * 6);
+ }
+
+ if (level == 1)
+ {
+ // Flash the last light.
+ _sections[0].PanelOverride = _timer > TimerCycle / 2 ? _styleBoxLit : _styleBoxUnlit;
+ }
+ else
+ {
+ _sections[0].PanelOverride = level > 2 ? _styleBoxLit : _styleBoxUnlit;
+ }
+
+ _sections[1].PanelOverride = level > 3 ? _styleBoxLit : _styleBoxUnlit;
+ _sections[2].PanelOverride = level > 4 ? _styleBoxLit : _styleBoxUnlit;
+ _sections[3].PanelOverride = level > 5 ? _styleBoxLit : _styleBoxUnlit;
+ _sections[4].PanelOverride = level > 6 ? _styleBoxLit : _styleBoxUnlit;
+ }
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/IItemStatus.cs b/Content.Client/GameObjects/Components/IItemStatus.cs
new file mode 100644
index 0000000000..a83e1be2fb
--- /dev/null
+++ b/Content.Client/GameObjects/Components/IItemStatus.cs
@@ -0,0 +1,33 @@
+using Robust.Client.UserInterface;
+
+namespace Content.Client.GameObjects.Components
+{
+ ///
+ /// Allows a component to provide status tooltips next to the hands interface.
+ ///
+ public interface IItemStatus
+ {
+ ///
+ /// Called to get a control that represents the status for this component.
+ ///
+ ///
+ /// The control to render as status.
+ ///
+ public Control MakeControl();
+
+ ///
+ /// Called when the item no longer needs this status (say, dropped from hand)
+ ///
+ ///
+ ///
+ /// Useful to allow you to drop the control for the GC, if you need to.
+ ///
+ ///
+ /// Note that this may be called after a second invocation of (for example if the user switches the item between two hands).
+ ///
+ ///
+ public void DestroyControl(Control control)
+ {
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Items/ItemStatusComponent.cs b/Content.Client/GameObjects/Components/Items/ItemStatusComponent.cs
new file mode 100644
index 0000000000..7d4562ce02
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Items/ItemStatusComponent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.GameObjects;
+
+namespace Content.Client.GameObjects
+{
+ [RegisterComponent]
+ public class ItemStatusComponent : Component
+ {
+ public override string Name => "ItemStatus";
+
+
+ }
+}
diff --git a/Content.Client/GameObjects/Components/StackComponent.cs b/Content.Client/GameObjects/Components/StackComponent.cs
new file mode 100644
index 0000000000..0d7fb2668b
--- /dev/null
+++ b/Content.Client/GameObjects/Components/StackComponent.cs
@@ -0,0 +1,60 @@
+using Content.Client.UserInterface;
+using Content.Client.Utility;
+using Content.Shared.GameObjects.Components;
+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;
+
+namespace Content.Client.GameObjects.Components
+{
+ [RegisterComponent]
+ public class StackComponent : SharedStackComponent, IItemStatus
+ {
+ [ViewVariables] public int Count { get; private set; }
+ [ViewVariables] public int MaxCount { get; private set; }
+
+ [ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
+
+ public Control MakeControl() => new StatusControl(this);
+
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ var cast = (StackComponentState) curState;
+
+ Count = cast.Count;
+ MaxCount = cast.MaxCount;
+ }
+
+ private sealed class StatusControl : Control
+ {
+ private readonly StackComponent _parent;
+ private readonly RichTextLabel _label;
+
+ public StatusControl(StackComponent parent)
+ {
+ _parent = parent;
+ _label = new RichTextLabel {StyleClasses = {NanoStyle.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("Count: [color=white]{0}[/color]", _parent.Count));
+ }
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponent.cs b/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponent.cs
new file mode 100644
index 0000000000..b933c51c0f
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponent.cs
@@ -0,0 +1,284 @@
+using System;
+using Content.Client.Animations;
+using Content.Client.UserInterface;
+using Content.Client.Utility;
+using Content.Shared.GameObjects;
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
+using Robust.Client.Animations;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Animations;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Network;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+using static Content.Client.StaticIoC;
+
+namespace Content.Client.GameObjects.Components.Weapons.Ranged
+{
+ [RegisterComponent]
+ public class BallisticMagazineWeaponComponent : Component, IItemStatus
+ {
+ private static readonly Animation AlarmAnimationSmg = new Animation
+ {
+ Length = TimeSpan.FromSeconds(1.4),
+ AnimationTracks =
+ {
+ new AnimationTrackControlProperty
+ {
+ // These timings match the SMG audio file.
+ Property = nameof(Label.FontColorOverride),
+ InterpolationMode = AnimationInterpolationMode.Previous,
+ KeyFrames =
+ {
+ new AnimationTrackProperty.KeyFrame(Color.Red, 0.1f),
+ new AnimationTrackProperty.KeyFrame(null, 0.3f),
+ new AnimationTrackProperty.KeyFrame(Color.Red, 0.2f),
+ new AnimationTrackProperty.KeyFrame(null, 0.3f),
+ new AnimationTrackProperty.KeyFrame(Color.Red, 0.2f),
+ new AnimationTrackProperty.KeyFrame(null, 0.3f),
+ }
+ }
+ }
+ };
+
+ private static readonly Animation AlarmAnimationLmg = new Animation
+ {
+ Length = TimeSpan.FromSeconds(0.75),
+ AnimationTracks =
+ {
+ new AnimationTrackControlProperty
+ {
+ // These timings match the SMG audio file.
+ Property = nameof(Label.FontColorOverride),
+ InterpolationMode = AnimationInterpolationMode.Previous,
+ KeyFrames =
+ {
+ new AnimationTrackProperty.KeyFrame(Color.Red, 0.0f),
+ new AnimationTrackProperty.KeyFrame(null, 0.15f),
+ new AnimationTrackProperty.KeyFrame(Color.Red, 0.15f),
+ new AnimationTrackProperty.KeyFrame(null, 0.15f),
+ new AnimationTrackProperty.KeyFrame(Color.Red, 0.15f),
+ new AnimationTrackProperty.KeyFrame(null, 0.15f),
+ }
+ }
+ }
+ };
+
+ public override string Name => "BallisticMagazineWeapon";
+ public override uint? NetID => ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON;
+ public override Type StateType => typeof(BallisticMagazineWeaponComponentState);
+
+ private StatusControl _statusControl;
+
+ ///
+ /// True if a bullet is chambered.
+ ///
+ [ViewVariables]
+ public bool Chambered { get; private set; }
+
+ ///
+ /// Count of bullets in the magazine.
+ ///
+ ///
+ /// Null if no magazine is inserted.
+ ///
+ [ViewVariables]
+ public (int count, int max)? MagazineCount { get; private set; }
+
+ [ViewVariables(VVAccess.ReadWrite)] private bool _isLmgAlarmAnimation;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _isLmgAlarmAnimation, "lmg_alarm_animation", false);
+ }
+
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ var cast = (BallisticMagazineWeaponComponentState) curState;
+
+ Chambered = cast.Chambered;
+ MagazineCount = cast.MagazineCount;
+ _statusControl?.Update();
+ }
+
+ public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null,
+ IComponent component = null)
+ {
+ switch (message)
+ {
+ case BmwComponentAutoEjectedMessage _:
+ _statusControl?.PlayAlarmAnimation();
+ return;
+ }
+
+ base.HandleMessage(message, netChannel, component);
+ }
+
+ public Control MakeControl()
+ {
+ _statusControl = new StatusControl(this);
+ _statusControl.Update();
+ return _statusControl;
+ }
+
+ public void DestroyControl(Control control)
+ {
+ if (_statusControl == control)
+ {
+ _statusControl = null;
+ }
+ }
+
+ private sealed class StatusControl : Control
+ {
+ private readonly BallisticMagazineWeaponComponent _parent;
+ private readonly HBoxContainer _bulletsListTop;
+ private readonly HBoxContainer _bulletsListBottom;
+ private readonly TextureRect _chamberedBullet;
+ private readonly Label _noMagazineLabel;
+
+ public StatusControl(BallisticMagazineWeaponComponent parent)
+ {
+ _parent = parent;
+ SizeFlagsHorizontal = SizeFlags.FillExpand;
+ SizeFlagsVertical = SizeFlags.ShrinkCenter;
+ AddChild(new VBoxContainer
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsVertical = SizeFlags.ShrinkCenter,
+ SeparationOverride = 0,
+ Children =
+ {
+ (_bulletsListTop = new HBoxContainer {SeparationOverride = 0}),
+ new HBoxContainer
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ Children =
+ {
+ new Control
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ Children =
+ {
+ (_bulletsListBottom = new HBoxContainer
+ {
+ SizeFlagsVertical = SizeFlags.ShrinkCenter,
+ SeparationOverride = 0
+ }),
+ (_noMagazineLabel = new Label
+ {
+ Text = "No Magazine!",
+ StyleClasses = {NanoStyle.StyleClassItemStatus}
+ })
+ }
+ },
+ (_chamberedBullet = new TextureRect
+ {
+ Texture = ResC.GetTexture("/Textures/UserInterface/status/bullets/chambered.png"),
+ SizeFlagsVertical = SizeFlags.ShrinkCenter,
+ SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Fill,
+ })
+ }
+ }
+ }
+ });
+ }
+
+ public void Update()
+ {
+ _chamberedBullet.ModulateSelfOverride =
+ _parent.Chambered ? Color.FromHex("#d7df60") : Color.Black;
+
+ _bulletsListTop.RemoveAllChildren();
+ _bulletsListBottom.RemoveAllChildren();
+
+ if (_parent.MagazineCount == null)
+ {
+ _noMagazineLabel.Visible = true;
+ return;
+ }
+
+ var (count, capacity) = _parent.MagazineCount.Value;
+
+ _noMagazineLabel.Visible = false;
+
+ string texturePath;
+ if (capacity <= 20)
+ {
+ texturePath = "/Textures/UserInterface/status/bullets/normal.png";
+ }
+ else if (capacity <= 30)
+ {
+ texturePath = "/Textures/UserInterface/status/bullets/small.png";
+ }
+ else
+ {
+ texturePath = "/Textures/UserInterface/status/bullets/tiny.png";
+ }
+
+ var texture = ResC.GetTexture(texturePath);
+
+ const int tinyMaxRow = 60;
+
+ if (capacity > tinyMaxRow)
+ {
+ FillBulletRow(_bulletsListBottom, Math.Min(tinyMaxRow, count), tinyMaxRow, texture);
+ FillBulletRow(_bulletsListTop, Math.Max(0, count - tinyMaxRow), capacity - tinyMaxRow, texture);
+ }
+ else
+ {
+ FillBulletRow(_bulletsListBottom, count, capacity, texture);
+ }
+ }
+
+ private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
+ {
+ var colorA = Color.FromHex("#b68f0e");
+ var colorB = Color.FromHex("#d7df60");
+ var colorGoneA = Color.FromHex("#000000");
+ var colorGoneB = Color.FromHex("#222222");
+
+ var altColor = false;
+
+ for (var i = count; i < capacity; i++)
+ {
+ container.AddChild(new TextureRect
+ {
+ Texture = texture,
+ ModulateSelfOverride = altColor ? colorGoneA : colorGoneB
+ });
+
+ altColor ^= true;
+ }
+
+ for (var i = 0; i < count; i++)
+ {
+ container.AddChild(new TextureRect
+ {
+ Texture = texture,
+ ModulateSelfOverride = altColor ? colorA : colorB
+ });
+
+ altColor ^= true;
+ }
+ }
+
+ protected override Vector2 CalculateMinimumSize()
+ {
+ return Vector2.ComponentMax((0, 15), base.CalculateMinimumSize());
+ }
+
+ public void PlayAlarmAnimation()
+ {
+ var animation = _parent._isLmgAlarmAnimation ? AlarmAnimationLmg : AlarmAnimationSmg;
+ _noMagazineLabel.PlayAnimation(animation, "alarm");
+ }
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/WelderComponent.cs b/Content.Client/GameObjects/Components/WelderComponent.cs
new file mode 100644
index 0000000000..3cb91380db
--- /dev/null
+++ b/Content.Client/GameObjects/Components/WelderComponent.cs
@@ -0,0 +1,74 @@
+using System;
+using Content.Client.UserInterface;
+using Content.Client.Utility;
+using Content.Shared.GameObjects;
+using Content.Shared.GameObjects.Components;
+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;
+
+namespace Content.Client.GameObjects.Components
+{
+ [RegisterComponent]
+ public class WelderComponent : Component, IItemStatus
+ {
+ public override string Name => "Welder";
+ public override uint? NetID => ContentNetIDs.WELDER;
+ public override Type StateType => typeof(WelderComponentState);
+
+ [ViewVariables] public float FuelCapacity { get; private set; }
+ [ViewVariables] public float Fuel { get; private set; }
+ [ViewVariables] public bool Activated { get; private set; }
+
+ [ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
+
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ var cast = (WelderComponentState) curState;
+
+ FuelCapacity = cast.FuelCapacity;
+ Fuel = cast.Fuel;
+ Activated = cast.Activated;
+
+ _uiUpdateNeeded = true;
+ }
+
+ public Control MakeControl() => new StatusControl(this);
+
+ private sealed class StatusControl : Control
+ {
+ private readonly WelderComponent _parent;
+ private readonly RichTextLabel _label;
+
+ public StatusControl(WelderComponent parent)
+ {
+ _parent = parent;
+ _label = new RichTextLabel {StyleClasses = {NanoStyle.StyleClassItemStatus}};
+ AddChild(_label);
+
+ parent._uiUpdateNeeded = true;
+ }
+
+ protected override void Update(FrameEventArgs args)
+ {
+ base.Update(args);
+
+ if (!_parent._uiUpdateNeeded)
+ {
+ return;
+ }
+
+ _parent._uiUpdateNeeded = false;
+
+ var fuelCap = _parent.FuelCapacity;
+ var fuel = _parent.Fuel;
+
+ _label.SetMarkup(Loc.GetString("Fuel: [color={0}]{1}/{2}[/color]",
+ fuel < fuelCap / 4f ? "darkorange" : "orange", Math.Round(fuel), fuelCap));
+ }
+ }
+ }
+}
diff --git a/Content.Client/StaticIoC.cs b/Content.Client/StaticIoC.cs
new file mode 100644
index 0000000000..21ea88d49c
--- /dev/null
+++ b/Content.Client/StaticIoC.cs
@@ -0,0 +1,10 @@
+using Robust.Client.Interfaces.ResourceManagement;
+using Robust.Shared.IoC;
+
+namespace Content.Client
+{
+ public static class StaticIoC
+ {
+ public static IResourceCache ResC => IoCManager.Resolve();
+ }
+}
diff --git a/Content.Client/UserInterface/GameHud.cs b/Content.Client/UserInterface/GameHud.cs
index c854b6177a..a9879263e4 100644
--- a/Content.Client/UserInterface/GameHud.cs
+++ b/Content.Client/UserInterface/GameHud.cs
@@ -214,11 +214,6 @@ namespace Content.Client.UserInterface
SizeFlagsVertical = Control.SizeFlags.ShrinkEnd
};
- HandsContainer = new MarginContainer
- {
- SizeFlagsVertical = Control.SizeFlags.ShrinkEnd
- };
-
_combatPanelContainer = new VBoxContainer
{
Children =
@@ -235,9 +230,20 @@ namespace Content.Client.UserInterface
_combatModeButton.OnToggled += args => OnCombatModeChanged?.Invoke(args.Pressed);
_targetingDoll.OnZoneChanged += args => OnTargetingZoneChanged?.Invoke(args);
- inventoryContainer.Children.Add(HandsContainer);
inventoryContainer.Children.Add(InventoryQuickButtonContainer);
inventoryContainer.Children.Add(_combatPanelContainer);
+
+
+ HandsContainer = new MarginContainer
+ {
+ SizeFlagsVertical = Control.SizeFlags.ShrinkEnd
+ };
+
+ RootControl.AddChild(HandsContainer);
+
+ LayoutContainer.SetAnchorAndMarginPreset(HandsContainer, LayoutContainer.LayoutPreset.CenterBottom);
+ LayoutContainer.SetGrowHorizontal(HandsContainer, LayoutContainer.GrowDirection.Both);
+ LayoutContainer.SetGrowVertical(HandsContainer, LayoutContainer.GrowDirection.Begin);
}
private void ButtonTutorialOnOnToggled()
diff --git a/Content.Client/UserInterface/HandsGui.cs b/Content.Client/UserInterface/HandsGui.cs
index e8fd13794f..227df1690a 100644
--- a/Content.Client/UserInterface/HandsGui.cs
+++ b/Content.Client/UserInterface/HandsGui.cs
@@ -23,9 +23,10 @@ namespace Content.Client.UserInterface
{
public class HandsGui : Control
{
+ private const string HandNameLeft = "left";
+ private const string HandNameRight = "right";
+
private const int CooldownLevels = 8;
- private const int BoxSpacing = 0;
- private const int BoxSize = 64;
#pragma warning disable 0649
[Dependency] private readonly IPlayerManager _playerManager;
@@ -40,8 +41,6 @@ namespace Content.Client.UserInterface
private IEntity LeftHand;
private IEntity RightHand;
- private UIBox2i _handL;
- private UIBox2i _handR;
private readonly SpriteView LeftSpriteView;
private readonly SpriteView RightSpriteView;
@@ -53,13 +52,13 @@ namespace Content.Client.UserInterface
private readonly Control _leftContainer;
private readonly Control _rightContainer;
+ private readonly ItemStatusPanel _rightStatusPanel;
+ private readonly ItemStatusPanel _leftStatusPanel;
+
public HandsGui()
{
IoCManager.InjectDependencies(this);
- _handR = new UIBox2i(0, 0, BoxSize, BoxSize);
- _handL = _handR.Translated((BoxSize + BoxSpacing, 0));
-
MouseFilter = MouseFilterMode.Stop;
TextureHandLeft = _resourceCache.GetTexture("/Textures/UserInterface/Inventory/hand_l.png");
@@ -73,30 +72,37 @@ namespace Content.Client.UserInterface
_resourceCache.GetTexture($"/Textures/UserInterface/Inventory/cooldown-{i}.png");
}
+ _rightStatusPanel = new ItemStatusPanel(true);
+ _leftStatusPanel = new ItemStatusPanel(false);
+
_leftContainer = new Control {MouseFilter = MouseFilterMode.Ignore};
_rightContainer = new Control {MouseFilter = MouseFilterMode.Ignore};
var hBox = new HBoxContainer
{
SeparationOverride = 0,
- Children = {_rightContainer, _leftContainer},
+ Children = {_rightStatusPanel, _rightContainer, _leftContainer, _leftStatusPanel},
MouseFilter = MouseFilterMode.Ignore
};
AddChild(hBox);
- _leftContainer.AddChild(new TextureRect
+ var textureLeft = new TextureRect
{
- MouseFilter = MouseFilterMode.Ignore,
Texture = TextureHandLeft,
TextureScale = (2, 2)
- });
+ };
+ textureLeft.OnKeyBindDown += args => HandKeyBindDown(args, HandNameLeft);
- _rightContainer.AddChild(new TextureRect
+ _leftContainer.AddChild(textureLeft);
+
+ var textureRight = new TextureRect
{
- MouseFilter = MouseFilterMode.Ignore,
Texture = TextureHandRight,
TextureScale = (2, 2)
- });
+ };
+ textureRight.OnKeyBindDown += args => HandKeyBindDown(args, HandNameRight);
+
+ _rightContainer.AddChild(textureRight);
_leftContainer.AddChild(ActiveHandRect = new TextureRect
{
@@ -127,6 +133,7 @@ namespace Content.Client.UserInterface
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
MouseFilter = MouseFilterMode.Ignore,
+ Stretch = TextureRect.StretchMode.KeepCentered,
TextureScale = (2, 2),
Visible = false,
});
@@ -136,6 +143,7 @@ namespace Content.Client.UserInterface
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
MouseFilter = MouseFilterMode.Ignore,
+ Stretch = TextureRect.StretchMode.KeepCentered,
TextureScale = (2, 2),
Visible = false
});
@@ -166,12 +174,13 @@ namespace Content.Client.UserInterface
if (!TryGetHands(out var hands))
return;
- var left = hands.GetEntity("left");
- var right = hands.GetEntity("right");
+ var left = hands.GetEntity(HandNameLeft);
+ var right = hands.GetEntity(HandNameRight);
ActiveHandRect.Parent.RemoveChild(ActiveHandRect);
- var parent = hands.ActiveIndex == "left" ? _leftContainer : _rightContainer;
+ var parent = hands.ActiveIndex == HandNameLeft ? _leftContainer : _rightContainer;
parent.AddChild(ActiveHandRect);
+ ActiveHandRect.SetPositionInParent(1);
if (left != null)
{
@@ -230,43 +239,13 @@ namespace Content.Client.UserInterface
hands.AttackByInHand(hand);
}
- protected override bool HasPoint(Vector2 point)
+ private void HandKeyBindDown(GUIBoundKeyEventArgs args, string handIndex)
{
- return _handL.Contains((Vector2i) point) || _handR.Contains((Vector2i) point);
- }
-
- protected override void KeyBindDown(GUIBoundKeyEventArgs args)
- {
- base.KeyBindDown(args);
-
- if (!args.CanFocus)
- {
- return;
- }
-
- var leftHandContains = _handL.Contains((Vector2i) args.RelativePosition);
- var rightHandContains = _handR.Contains((Vector2i) args.RelativePosition);
-
- string handIndex;
- if (leftHandContains)
- {
- handIndex = "left";
- }
- else if (rightHandContains)
- {
- handIndex = "right";
- }
- else
- {
- return;
- }
-
if (args.Function == EngineKeyFunctions.Use)
{
if (!TryGetHands(out var hands))
return;
-
if (hands.ActiveIndex == handIndex)
{
UseActiveHand();
@@ -276,21 +255,18 @@ namespace Content.Client.UserInterface
AttackByInHand(handIndex);
}
}
-
else if (args.Function == ContentKeyFunctions.ExamineEntity)
{
var examine = IoCManager.Resolve().GetEntitySystem();
- if (leftHandContains)
+ if (handIndex == HandNameLeft)
examine.DoExamine(LeftHand);
- else if (rightHandContains)
+ else if (handIndex == HandNameRight)
examine.DoExamine(RightHand);
}
-
else if (args.Function == ContentKeyFunctions.MouseMiddle)
{
SendSwitchHandTo(handIndex);
}
-
else if (args.Function == ContentKeyFunctions.OpenContextMenu)
{
if (!TryGetHands(out var hands))
@@ -316,6 +292,9 @@ namespace Content.Client.UserInterface
UpdateCooldown(CooldownCircleLeft, LeftHand);
UpdateCooldown(CooldownCircleRight, RightHand);
+
+ _rightStatusPanel.Update(RightHand);
+ _leftStatusPanel.Update(LeftHand);
}
private void UpdateCooldown(TextureRect cooldownTexture, IEntity entity)
diff --git a/Content.Client/UserInterface/ItemStatusPanel.cs b/Content.Client/UserInterface/ItemStatusPanel.cs
new file mode 100644
index 0000000000..e53a1cd522
--- /dev/null
+++ b/Content.Client/UserInterface/ItemStatusPanel.cs
@@ -0,0 +1,121 @@
+using System.Collections.Generic;
+using Content.Client.GameObjects.Components;
+using Content.Client.Utility;
+using Robust.Client.Graphics.Drawing;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Maths;
+using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
+using static Content.Client.StaticIoC;
+
+namespace Content.Client.UserInterface
+{
+ public class ItemStatusPanel : Control
+ {
+ [ViewVariables]
+ private readonly List<(IItemStatus, Control)> _activeStatusComponents = new List<(IItemStatus, Control)>();
+
+ [ViewVariables]
+ private readonly Label _itemNameLabel;
+ [ViewVariables]
+ private readonly VBoxContainer _statusContents;
+ [ViewVariables]
+ private readonly PanelContainer _panel;
+
+ [ViewVariables]
+ private IEntity _entity;
+
+ public ItemStatusPanel(bool isRightHand)
+ {
+ // isRightHand means on the LEFT of the screen.
+ // Keep that in mind.
+ var panel = new StyleBoxTexture
+ {
+ Texture = ResC.GetTexture(isRightHand
+ ? "/Nano/item_status_right.svg.96dpi.png"
+ : "/Nano/item_status_left.svg.96dpi.png")
+ };
+ panel.SetContentMarginOverride(StyleBox.Margin.Vertical, 4);
+ panel.SetContentMarginOverride(StyleBox.Margin.Horizontal, 6);
+ panel.SetPatchMargin((isRightHand ? StyleBox.Margin.Left : StyleBox.Margin.Right) | StyleBox.Margin.Top,
+ 13);
+
+ AddChild(_panel = new PanelContainer
+ {
+ PanelOverride = panel,
+ ModulateSelfOverride = Color.White.WithAlpha(0.9f),
+ Children =
+ {
+ new VBoxContainer
+ {
+ SeparationOverride = 0,
+ Children =
+ {
+ (_statusContents = new VBoxContainer()),
+ (_itemNameLabel = new Label
+ {
+ ClipText = true,
+ StyleClasses = {NanoStyle.StyleClassItemStatus}
+ })
+ }
+ }
+ }
+ });
+ SizeFlagsVertical = SizeFlags.ShrinkEnd;
+ }
+
+ public void Update(IEntity entity)
+ {
+ if (entity == null)
+ {
+ ClearOldStatus();
+ _entity = null;
+ _panel.Visible = false;
+ return;
+ }
+
+ if (entity != _entity)
+ {
+ _entity = entity;
+ BuildNewEntityStatus();
+ }
+
+ _panel.Visible = true;
+ _itemNameLabel.Text = entity.Name;
+ }
+
+ private void ClearOldStatus()
+ {
+ _statusContents.RemoveAllChildren();
+
+ foreach (var (itemStatus, control) in _activeStatusComponents)
+ {
+ itemStatus.DestroyControl(control);
+ }
+
+ _activeStatusComponents.Clear();
+ }
+
+ private void BuildNewEntityStatus()
+ {
+ DebugTools.AssertNotNull(_entity);
+
+ ClearOldStatus();
+
+ foreach (var statusComponent in _entity.GetAllComponents())
+ {
+ var control = statusComponent.MakeControl();
+ _statusContents.AddChild(control);
+
+ _activeStatusComponents.Add((statusComponent, control));
+ }
+ }
+
+ protected override Vector2 CalculateMinimumSize()
+ {
+ return Vector2.ComponentMax(base.CalculateMinimumSize(), (150, 00));
+ }
+ }
+}
diff --git a/Content.Client/UserInterface/NanoStyle.cs b/Content.Client/UserInterface/NanoStyle.cs
index 32de2818cb..1dbd34fff6 100644
--- a/Content.Client/UserInterface/NanoStyle.cs
+++ b/Content.Client/UserInterface/NanoStyle.cs
@@ -30,11 +30,14 @@ namespace Content.Client.UserInterface
public const string StyleClassPowerStateLow = "PowerStateLow";
public const string StyleClassPowerStateGood = "PowerStateGood";
+ public const string StyleClassItemStatus = "ItemStatus";
+
public Stylesheet Stylesheet { get; }
public NanoStyle()
{
var resCache = IoCManager.Resolve();
+ var notoSans8 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 8);
var notoSans10 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 10);
var notoSans12 = resCache.GetFont("/Nano/NotoSans/NotoSans-Regular.ttf", 12);
var notoSansBold12 = resCache.GetFont("/Nano/NotoSans/NotoSans-Bold.ttf", 12);
@@ -108,7 +111,8 @@ namespace Content.Client.UserInterface
var vScrollBarGrabberNormal = new StyleBoxFlat
{
- BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginLeftOverride = 10, ContentMarginTopOverride = 10
+ BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginLeftOverride = 10,
+ ContentMarginTopOverride = 10
};
var vScrollBarGrabberHover = new StyleBoxFlat
{
@@ -175,7 +179,7 @@ namespace Content.Client.UserInterface
var itemListItemBackground = new StyleBoxFlat {BackgroundColor = new Color(55, 55, 68)};
itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
- var itemListItemBackgroundTransparent = new StyleBoxFlat { BackgroundColor = Color.Transparent };
+ var itemListItemBackgroundTransparent = new StyleBoxFlat {BackgroundColor = Color.Transparent};
itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
@@ -437,7 +441,7 @@ namespace Content.Client.UserInterface
itemListBackgroundSelected)
}),
- new StyleRule(new SelectorElement(typeof(ItemList), new []{"transparentItemList"}, null, null), new[]
+ new StyleRule(new SelectorElement(typeof(ItemList), new[] {"transparentItemList"}, null, null), new[]
{
new StyleProperty(ItemList.StylePropertyBackground,
new StyleBoxFlat {BackgroundColor = Color.Transparent}),
@@ -482,11 +486,12 @@ namespace Content.Client.UserInterface
}),
// Bigger Label
- new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassLabelHeadingBigger}, null, null), new[]
- {
- new StyleProperty(Label.StylePropertyFont, notoSansBold20),
- new StyleProperty(Label.StylePropertyFontColor, NanoGold),
- }),
+ new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassLabelHeadingBigger}, null, null),
+ new[]
+ {
+ new StyleProperty(Label.StylePropertyFont, notoSansBold20),
+ new StyleProperty(Label.StylePropertyFontColor, NanoGold),
+ }),
// Small Label
new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassLabelSubText}, null, null), new[]
@@ -496,17 +501,18 @@ namespace Content.Client.UserInterface
}),
// Label Key
- new StyleRule(new SelectorElement(typeof(Label), new []{StyleClassLabelKeyText}, null, null), new []
+ new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassLabelKeyText}, null, null), new[]
{
new StyleProperty(Label.StylePropertyFont, notoSansBold12),
new StyleProperty(Label.StylePropertyFontColor, NanoGold)
}),
- new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassLabelSecondaryColor}, null, null), new[]
- {
- new StyleProperty(Label.StylePropertyFont, notoSans12),
- new StyleProperty(Label.StylePropertyFontColor, Color.DarkGray),
- }),
+ new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassLabelSecondaryColor}, null, null),
+ new[]
+ {
+ new StyleProperty(Label.StylePropertyFont, notoSans12),
+ new StyleProperty(Label.StylePropertyFontColor, Color.DarkGray),
+ }),
// Big Button
new StyleRule(new SelectorElement(typeof(Button), new[] {StyleClassButtonBig}, null, null), new[]
@@ -596,7 +602,7 @@ namespace Content.Client.UserInterface
// StripeBack
new StyleRule(
SelectorElement.Type(typeof(StripeBack)),
- new []
+ new[]
{
new StyleProperty(StripeBack.StylePropertyBackground, stripeBack),
}),
@@ -604,10 +610,16 @@ namespace Content.Client.UserInterface
// StyleClassLabelBig
new StyleRule(
SelectorElement.Class(StyleClassLabelBig),
- new []
+ new[]
{
new StyleProperty("font", notoSans16),
}),
+
+ // StyleClassItemStatus
+ new StyleRule(SelectorElement.Class(StyleClassItemStatus), new[]
+ {
+ new StyleProperty("font", notoSans10),
+ }),
});
}
}
diff --git a/Content.Client/Utility/RichTextLabelExt.cs b/Content.Client/Utility/RichTextLabelExt.cs
new file mode 100644
index 0000000000..15b9944af8
--- /dev/null
+++ b/Content.Client/Utility/RichTextLabelExt.cs
@@ -0,0 +1,13 @@
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Utility
+{
+ public static class RichTextLabelExt
+ {
+ public static void SetMarkup(this RichTextLabel label, string markup)
+ {
+ label.SetMessage(FormattedMessage.FromMarkup(markup));
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Interactable/HandheldLightComponent.cs b/Content.Server/GameObjects/Components/Interactable/HandheldLightComponent.cs
index 8ddcb10d65..5fd7e39bd6 100644
--- a/Content.Server/GameObjects/Components/Interactable/HandheldLightComponent.cs
+++ b/Content.Server/GameObjects/Components/Interactable/HandheldLightComponent.cs
@@ -3,6 +3,7 @@ using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects;
using Content.Shared.GameObjects;
+using Content.Shared.GameObjects.Components;
using Content.Shared.Interfaces;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
@@ -20,7 +21,7 @@ namespace Content.Server.GameObjects.Components.Interactable
/// Component that represents a handheld lightsource which can be toggled on and off.
///
[RegisterComponent]
- internal class HandheldLightComponent : Component, IUse, IExamine, IAttackBy, IMapInit
+ internal sealed class HandheldLightComponent : SharedHandheldLightComponent, IUse, IExamine, IAttackBy, IMapInit
{
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _notifyManager;
@@ -44,9 +45,6 @@ namespace Content.Server.GameObjects.Components.Interactable
return cell;
}
}
-
-
- public override string Name => "HandheldLight";
///
/// Status of light, whether or not it is emitting light.
@@ -73,7 +71,6 @@ namespace Content.Server.GameObjects.Components.Interactable
}
return true;
-
}
void IExamine.Examine(FormattedMessage message)
@@ -153,6 +150,7 @@ namespace Content.Server.GameObjects.Components.Interactable
{
soundComponent.Play("/Audio/machines/button.ogg");
}
+
_notifyManager.PopupMessage(Owner, user, _localizationManager.GetString("Cell missing..."));
return;
}
@@ -166,6 +164,7 @@ namespace Content.Server.GameObjects.Components.Interactable
{
soundComponent.Play("/Audio/machines/button.ogg");
}
+
_notifyManager.PopupMessage(Owner, user, _localizationManager.GetString("Dead cell..."));
return;
}
@@ -195,6 +194,8 @@ namespace Content.Server.GameObjects.Components.Interactable
var cell = Cell;
if (cell == null || !cell.TryDeductWattage(Wattage, frameTime)) TurnOff();
+
+ Dirty();
}
private void EjectCell(IEntity user)
@@ -227,6 +228,23 @@ namespace Content.Server.GameObjects.Components.Interactable
}
}
+ public override ComponentState GetComponentState()
+ {
+ if (Cell == null)
+ {
+ return new HandheldLightComponentState(null);
+ }
+
+ if (Cell.AvailableCharge(1) < Wattage)
+ {
+ // Practically zero.
+ // This is so the item status works correctly.
+ return new HandheldLightComponentState(0);
+ }
+
+ return new HandheldLightComponentState(Cell.Charge / Cell.Capacity);
+ }
+
[Verb]
public sealed class EjectCellVerb : Verb
{
@@ -252,6 +270,7 @@ namespace Content.Server.GameObjects.Components.Interactable
{
return;
}
+
var cell = Owner.EntityManager.SpawnEntity("PowerCellSmallHyper", Owner.Transform.GridPosition);
_cellContainer.Insert(cell);
}
diff --git a/Content.Server/GameObjects/Components/Interactable/Tools/WelderComponent.cs b/Content.Server/GameObjects/Components/Interactable/Tools/WelderComponent.cs
index 661c3a11b7..e2fa2791fc 100644
--- a/Content.Server/GameObjects/Components/Interactable/Tools/WelderComponent.cs
+++ b/Content.Server/GameObjects/Components/Interactable/Tools/WelderComponent.cs
@@ -1,6 +1,8 @@
using System;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Audio;
+using Content.Shared.GameObjects;
+using Content.Shared.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
@@ -32,6 +34,7 @@ namespace Content.Server.GameObjects.Components.Interactable.Tools
#pragma warning restore 649
public override string Name => "Welder";
+ public override uint? NetID => ContentNetIDs.WELDER;
///
/// Maximum fuel capacity the welder can hold
@@ -40,8 +43,13 @@ namespace Content.Server.GameObjects.Components.Interactable.Tools
public float FuelCapacity
{
get => _fuelCapacity;
- set => _fuelCapacity = value;
+ set
+ {
+ _fuelCapacity = value;
+ Dirty();
+ }
}
+
private float _fuelCapacity = 50;
///
@@ -51,9 +59,15 @@ namespace Content.Server.GameObjects.Components.Interactable.Tools
public float Fuel
{
get => _fuel;
- set => _fuel = value;
+ set
+ {
+ _fuel = value;
+ Dirty();
+ }
}
+
private float _fuel = 0;
+ private bool _activated = false;
///
/// Default Cost of using the welder fuel for an action
@@ -69,7 +83,15 @@ namespace Content.Server.GameObjects.Components.Interactable.Tools
/// Status of welder, whether it is ignited
///
[ViewVariables]
- public bool Activated { get; private set; } = false;
+ public bool Activated
+ {
+ get => _activated;
+ private set
+ {
+ _activated = value;
+ Dirty();
+ }
+ }
//private string OnSprite { get; set; }
//private string OffSprite { get; set; }
@@ -87,6 +109,7 @@ namespace Content.Server.GameObjects.Components.Interactable.Tools
serializer.DataField(ref _fuelCapacity, "Capacity", 50);
serializer.DataField(ref _fuel, "Fuel", FuelCapacity);
+ serializer.DataField(ref _activated, "Activated", false);
}
public void OnUpdate(float frameTime)
@@ -185,5 +208,10 @@ namespace Content.Server.GameObjects.Components.Interactable.Tools
_entitySystemManager.GetEntitySystem()
.Play(file, AudioParams.Default.WithVolume(volume));
}
+
+ public override ComponentState GetComponentState()
+ {
+ return new WelderComponentState(FuelCapacity, Fuel, Activated);
+ }
}
}
diff --git a/Content.Server/GameObjects/Components/Stack/StackComponent.cs b/Content.Server/GameObjects/Components/Stack/StackComponent.cs
index 350a297064..d18895c9c3 100644
--- a/Content.Server/GameObjects/Components/Stack/StackComponent.cs
+++ b/Content.Server/GameObjects/Components/Stack/StackComponent.cs
@@ -1,5 +1,6 @@
using System;
using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.GameObjects.Components;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.Reflection;
@@ -16,7 +17,7 @@ namespace Content.Server.GameObjects.Components.Stack
// TODO: Naming and presentation and such could use some improvement.
[RegisterComponent]
- public class StackComponent : Component, IAttackBy, IExamine
+ public class StackComponent : SharedStackComponent, IAttackBy, IExamine
{
#pragma warning disable 649
[Dependency] private readonly ISharedNotifyManager _sharedNotifyManager;
@@ -26,8 +27,6 @@ namespace Content.Server.GameObjects.Components.Stack
private int _count = 50;
private int _maxCount = 50;
- public override string Name => "Stack";
-
[ViewVariables(VVAccess.ReadWrite)]
public int Count
{
@@ -39,11 +38,20 @@ namespace Content.Server.GameObjects.Components.Stack
{
Owner.Delete();
}
+ Dirty();
}
}
[ViewVariables]
- public int MaxCount { get => _maxCount; private set => _maxCount = value; }
+ public int MaxCount
+ {
+ get => _maxCount;
+ private set
+ {
+ _maxCount = value;
+ Dirty();
+ }
+ }
[ViewVariables]
public int AvailableSpace => MaxCount - Count;
@@ -155,6 +163,11 @@ namespace Content.Server.GameObjects.Components.Stack
"There is [color=lightgray]1[/color] thing in the stack",
"There are [color=lightgray]{0}[/color] things in the stack.", Count, Count));
}
+
+ public override ComponentState GetComponentState()
+ {
+ return new StackComponentState(Count, MaxCount);
+ }
}
public enum StackType
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
index 6c9c4996b5..d38bc355e6 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/BallisticMagazineWeaponComponent.cs
@@ -24,33 +24,27 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
[RegisterComponent]
public class BallisticMagazineWeaponComponent : BallisticWeaponComponent, IUse, IAttackBy, IMapInit
{
+ private const float BulletOffset = 0.2f;
+
public override string Name => "BallisticMagazineWeapon";
+ public override uint? NetID => ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON;
- [ViewVariables]
- private string _defaultMagazine;
+ [ViewVariables] private string _defaultMagazine;
- [ViewVariables]
- private ContainerSlot _magazineSlot;
+ [ViewVariables] private ContainerSlot _magazineSlot;
private List _magazineTypes;
- [ViewVariables]
- public List MagazineTypes => _magazineTypes;
- [ViewVariables]
- private IEntity Magazine => _magazineSlot.ContainedEntity;
+ [ViewVariables] public List MagazineTypes => _magazineTypes;
+ [ViewVariables] private IEntity Magazine => _magazineSlot.ContainedEntity;
#pragma warning disable 649
[Dependency] private readonly IRobustRandom _bulletDropRandom;
#pragma warning restore 649
- [ViewVariables]
- private string _magInSound;
- [ViewVariables]
- private string _magOutSound;
- [ViewVariables]
- private string _autoEjectSound;
- [ViewVariables]
- private bool _autoEjectMagazine;
- [ViewVariables]
- private AppearanceComponent _appearance;
+ [ViewVariables] private string _magInSound;
+ [ViewVariables] private string _magOutSound;
+ [ViewVariables] private string _autoEjectSound;
+ [ViewVariables] private bool _autoEjectMagazine;
+ [ViewVariables] private AppearanceComponent _appearance;
private static readonly Direction[] _randomBulletDirs =
{
@@ -67,7 +61,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
base.ExposeData(serializer);
serializer.DataField(ref _magazineTypes, "magazines",
- new List{BallisticMagazineType.Unspecified});
+ new List {BallisticMagazineType.Unspecified});
serializer.DataField(ref _defaultMagazine, "default_magazine", null);
serializer.DataField(ref _autoEjectMagazine, "auto_eject_magazine", false);
serializer.DataField(ref _autoEjectSound, "sound_auto_eject", null);
@@ -137,6 +131,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
}
_updateAppearance();
+ Dirty();
return true;
}
@@ -153,15 +148,17 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
entity.Transform.GridPosition = Owner.Transform.GridPosition;
if (_magOutSound != null)
{
- Owner.GetComponent().Play(_magOutSound);
+ Owner.GetComponent().Play(_magOutSound, AudioParams.Default.WithVolume(20));
}
_updateAppearance();
+ Dirty();
entity.GetComponent().OnAmmoCountChanged -= _magazineAmmoCountChanged;
return true;
}
_updateAppearance();
+ Dirty();
return false;
}
@@ -171,7 +168,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
// Eject chambered bullet.
var entity = RemoveFromChamber(chamber);
- entity.Transform.GridPosition = Owner.Transform.GridPosition;
+ var offsetPos = (CalcBulletOffset(), CalcBulletOffset());
+ entity.Transform.GridPosition = Owner.Transform.GridPosition.Offset(offsetPos);
entity.Transform.LocalRotation = _bulletDropRandom.Pick(_randomBulletDirs).ToAngle();
var effect = $"/Audio/Guns/Casings/casingfall{_bulletDropRandom.Next(1, 4)}.ogg";
Owner.GetComponent().Play(effect, AudioParams.Default.WithVolume(-3));
@@ -187,17 +185,31 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
if (magComponent.CountLoaded == 0 && _autoEjectMagazine)
{
- EjectMagazine();
- if (_autoEjectSound != null)
- {
- Owner.GetComponent().Play(_autoEjectSound, AudioParams.Default.WithVolume(-5));
- }
+ DoAutoEject();
}
}
+ Dirty();
_updateAppearance();
}
+ private float CalcBulletOffset()
+ {
+ return _bulletDropRandom.NextFloat() * (BulletOffset * 2) - BulletOffset;
+ }
+
+ private void DoAutoEject()
+ {
+ SendNetworkMessage(new BmwComponentAutoEjectedMessage());
+ EjectMagazine();
+ if (_autoEjectSound != null)
+ {
+ Owner.GetComponent().Play(_autoEjectSound, AudioParams.Default.WithVolume(-5));
+ }
+
+ Dirty();
+ }
+
public bool UseEntity(UseEntityEventArgs eventArgs)
{
var ret = EjectMagazine();
@@ -237,6 +249,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
private void _magazineAmmoCountChanged()
{
+ Dirty();
_updateAppearance();
}
@@ -257,6 +270,21 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
}
}
+ public override ComponentState GetComponentState()
+ {
+ var chambered = GetChambered(0) != null;
+
+ (int, int)? count = null;
+
+ if (Magazine != null)
+ {
+ var magComponent = Magazine.GetComponent();
+ count = (magComponent.CountLoaded, magComponent.Capacity);
+ }
+
+ return new BallisticMagazineWeaponComponentState(chambered, count);
+ }
+
[Verb]
public sealed class EjectMagazineVerb : Verb
{
diff --git a/Content.Shared/GameObjects/Components/SharedHandheldLightComponent.cs b/Content.Shared/GameObjects/Components/SharedHandheldLightComponent.cs
new file mode 100644
index 0000000000..c6b5eab02a
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/SharedHandheldLightComponent.cs
@@ -0,0 +1,24 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components
+{
+ public abstract class SharedHandheldLightComponent : Component
+ {
+ public sealed override string Name => "HandheldLight";
+ public sealed override uint? NetID => ContentNetIDs.HANDHELD_LIGHT;
+ public sealed override Type StateType => typeof(HandheldLightComponentState);
+
+ [Serializable, NetSerializable]
+ protected sealed class HandheldLightComponentState : ComponentState
+ {
+ public HandheldLightComponentState(float? charge) : base(ContentNetIDs.HANDHELD_LIGHT)
+ {
+ Charge = charge;
+ }
+
+ public float? Charge { get; }
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/SharedStackComponent.cs b/Content.Shared/GameObjects/Components/SharedStackComponent.cs
new file mode 100644
index 0000000000..006ef53565
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/SharedStackComponent.cs
@@ -0,0 +1,26 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components
+{
+ public abstract class SharedStackComponent : Component
+ {
+ public sealed override string Name => "Stack";
+ public sealed override uint? NetID => ContentNetIDs.STACK;
+ public sealed override Type StateType => typeof(StackComponentState);
+
+ [Serializable, NetSerializable]
+ protected sealed class StackComponentState : ComponentState
+ {
+ public int Count { get; }
+ public int MaxCount { get; }
+
+ public StackComponentState(int count, int maxCount) : base(ContentNetIDs.STACK)
+ {
+ Count = count;
+ MaxCount = maxCount;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponentState.cs b/Content.Shared/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponentState.cs
new file mode 100644
index 0000000000..158dbaa5ad
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponentState.cs
@@ -0,0 +1,39 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components.Weapons.Ranged
+{
+ [Serializable, NetSerializable]
+ public class BallisticMagazineWeaponComponentState : ComponentState
+ {
+ ///
+ /// True if a bullet is chambered.
+ ///
+ public bool Chambered { get; }
+
+ ///
+ /// Count of bullets in the magazine.
+ ///
+ ///
+ /// Null if no magazine is inserted.
+ ///
+ public (int count, int max)? MagazineCount { get; }
+
+ public BallisticMagazineWeaponComponentState(bool chambered, (int count, int max)? magazineCount) : base(ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON)
+ {
+ Chambered = chambered;
+ MagazineCount = magazineCount;
+ }
+ }
+
+ // BMW is "Ballistic Magazine Weapon" here.
+ ///
+ /// Fired server -> client when the magazine in a Ballistic Magazine Weapon got auto-ejected.
+ ///
+ [Serializable, NetSerializable]
+ public sealed class BmwComponentAutoEjectedMessage : ComponentMessage
+ {
+
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/WelderComponentState.cs b/Content.Shared/GameObjects/Components/WelderComponentState.cs
new file mode 100644
index 0000000000..61aaa5ed1b
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/WelderComponentState.cs
@@ -0,0 +1,21 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components
+{
+ [NetSerializable, Serializable]
+ public class WelderComponentState : ComponentState
+ {
+ public float FuelCapacity { get; }
+ public float Fuel { get; }
+ public bool Activated { get; }
+
+ public WelderComponentState(float fuelCapacity, float fuel, bool activated) : base(ContentNetIDs.WELDER)
+ {
+ FuelCapacity = fuelCapacity;
+ Fuel = fuel;
+ Activated = activated;
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs
index 76bed8164f..599c7a3125 100644
--- a/Content.Shared/GameObjects/ContentNetIDs.cs
+++ b/Content.Shared/GameObjects/ContentNetIDs.cs
@@ -36,5 +36,9 @@
public const uint GALACTIC_MARKET = 1031;
public const uint HAIR = 1032;
public const uint INSTRUMENTS = 1033;
+ public const uint WELDER = 1034;
+ public const uint STACK = 1035;
+ public const uint HANDHELD_LIGHT = 1036;
+ public const uint BALLISTIC_MAGAZINE_WEAPON = 1037;
}
}
diff --git a/Resources/Nano/item_status_left.svg b/Resources/Nano/item_status_left.svg
new file mode 100644
index 0000000000..25c14b0ecd
--- /dev/null
+++ b/Resources/Nano/item_status_left.svg
@@ -0,0 +1,100 @@
+
+
+
+
diff --git a/Resources/Nano/item_status_left.svg.96dpi.png b/Resources/Nano/item_status_left.svg.96dpi.png
new file mode 100644
index 0000000000..9fb2e17484
Binary files /dev/null and b/Resources/Nano/item_status_left.svg.96dpi.png differ
diff --git a/Resources/Nano/item_status_right.svg b/Resources/Nano/item_status_right.svg
new file mode 100644
index 0000000000..3d4183738c
--- /dev/null
+++ b/Resources/Nano/item_status_right.svg
@@ -0,0 +1,105 @@
+
+
+
+
diff --git a/Resources/Nano/item_status_right.svg.96dpi.png b/Resources/Nano/item_status_right.svg.96dpi.png
new file mode 100644
index 0000000000..35e200c21f
Binary files /dev/null and b/Resources/Nano/item_status_right.svg.96dpi.png differ
diff --git a/Resources/Prototypes/Entities/Weapons/Rifles/rifles.yml b/Resources/Prototypes/Entities/Weapons/Rifles/rifles.yml
index 0fb7aa9037..21d2e0871b 100644
--- a/Resources/Prototypes/Entities/Weapons/Rifles/rifles.yml
+++ b/Resources/Prototypes/Entities/Weapons/Rifles/rifles.yml
@@ -7,7 +7,7 @@
- type: Sound
- type: BallisticMagazineWeapon
caliber: A762mm
- magazines:
+ magazines:
- A762mm
default_magazine: magazine_762mm_filled
auto_eject_magazine: false
@@ -36,7 +36,7 @@
firerate: 8
- type: BallisticMagazineWeapon
caliber: A762mm
- magazines:
+ magazines:
- A762mm
default_magazine: magazine_ak
sound_gunshot: /Audio/Guns/Gunshots/rifle2.ogg
@@ -69,7 +69,7 @@
firerate: 8
- type: BallisticMagazineWeapon
caliber: A762mm
- magazines:
+ magazines:
- A762mm
default_magazine: magazine_ak
sound_gunshot: /Audio/Guns/Gunshots/rifle2.ogg
@@ -102,7 +102,7 @@
firerate: 6
- type: BallisticMagazineWeapon
caliber: A10mm
- magazines:
+ magazines:
- A10mmSMG
default_magazine: magazine_10mm_smg
sound_gunshot: /Audio/Guns/Gunshots/rifle2.ogg
@@ -135,7 +135,7 @@
firerate: 8
- type: BallisticMagazineWeapon
caliber: A24mm
- magazines:
+ magazines:
- A24mm
default_magazine: magazine_24mm
auto_eject_magazine: true
@@ -169,7 +169,7 @@
firerate: 6
- type: BallisticMagazineWeapon
caliber: A10mm
- magazines:
+ magazines:
- A10mmSMG
default_magazine: magazine_10mm_smg
auto_eject_magazine: true
@@ -202,10 +202,11 @@
firerate: 8
- type: BallisticMagazineWeapon
caliber: A65mm
- magazines:
+ magazines:
- A65mm
default_magazine: magazine_65mm
auto_eject_magazine: true
+ lmg_alarm_animation: true
sound_auto_eject: /Audio/Guns/EmptyAlarm/lmg_empty_alarm.ogg
sound_gunshot: /Audio/Guns/Gunshots/rifle.ogg
- type: Appearance
@@ -236,10 +237,11 @@
firerate: 8
- type: BallisticMagazineWeapon
caliber: A65mm
- magazines:
+ magazines:
- A65mm
default_magazine: magazine_65mm
auto_eject_magazine: true
+ lmg_alarm_animation: true
sound_auto_eject: /Audio/Guns/EmptyAlarm/lmg_empty_alarm.ogg
sound_gunshot: /Audio/Guns/Gunshots/rifle.ogg
- type: Appearance
@@ -270,7 +272,7 @@
firerate: 6
- type: BallisticMagazineWeapon
caliber: A65mm
- magazines:
+ magazines:
- A65mm
default_magazine: magazine_65mm
sound_gunshot: /Audio/Guns/Gunshots/rifle2.ogg
@@ -281,4 +283,4 @@
steps: 2
- type: Item
Size: 24
- sprite: Objects/Guns/Rifles/sts.rsi
\ No newline at end of file
+ sprite: Objects/Guns/Rifles/sts.rsi
diff --git a/Resources/Prototypes/Entities/items/materials.yml b/Resources/Prototypes/Entities/items/materials.yml
index 29a546e890..20591d84a5 100644
--- a/Resources/Prototypes/Entities/items/materials.yml
+++ b/Resources/Prototypes/Entities/items/materials.yml
@@ -5,6 +5,7 @@
components:
- type: Stack
- type: Material
+ - type: ItemStatus
- type: entity
name: Steel Sheet
diff --git a/Resources/Prototypes/Entities/items/tools.yml b/Resources/Prototypes/Entities/items/tools.yml
index 47d69fba5a..b3e18ca422 100644
--- a/Resources/Prototypes/Entities/items/tools.yml
+++ b/Resources/Prototypes/Entities/items/tools.yml
@@ -27,7 +27,7 @@
- type: MeleeWeapon
- type: entity
- name: Welder
+ name: Welding Tool
parent: BaseItem
id: Welder
description: Melts anything as long as it's fueled, don't forget your eye protection!
@@ -46,6 +46,7 @@
state: welder
- type: ItemCooldown
- type: MeleeWeapon
+ - type: ItemStatus
- type: entity
name: Wrench
diff --git a/Resources/Textures/UserInterface/Inventory/hand_l.png b/Resources/Textures/UserInterface/Inventory/hand_l.png
index fef9589a4a..c760ae8ee8 100644
Binary files a/Resources/Textures/UserInterface/Inventory/hand_l.png and b/Resources/Textures/UserInterface/Inventory/hand_l.png differ
diff --git a/Resources/Textures/UserInterface/Inventory/hand_r.png b/Resources/Textures/UserInterface/Inventory/hand_r.png
index ae692d32b1..b338544131 100644
Binary files a/Resources/Textures/UserInterface/Inventory/hand_r.png and b/Resources/Textures/UserInterface/Inventory/hand_r.png differ
diff --git a/Resources/Textures/UserInterface/status/bullets/chambered.png b/Resources/Textures/UserInterface/status/bullets/chambered.png
new file mode 100644
index 0000000000..02d6bb406a
Binary files /dev/null and b/Resources/Textures/UserInterface/status/bullets/chambered.png differ
diff --git a/Resources/Textures/UserInterface/status/bullets/normal.png b/Resources/Textures/UserInterface/status/bullets/normal.png
new file mode 100644
index 0000000000..c9f1b34f91
Binary files /dev/null and b/Resources/Textures/UserInterface/status/bullets/normal.png differ
diff --git a/Resources/Textures/UserInterface/status/bullets/small.png b/Resources/Textures/UserInterface/status/bullets/small.png
new file mode 100644
index 0000000000..66215c85e3
Binary files /dev/null and b/Resources/Textures/UserInterface/status/bullets/small.png differ
diff --git a/Resources/Textures/UserInterface/status/bullets/tiny.png b/Resources/Textures/UserInterface/status/bullets/tiny.png
new file mode 100644
index 0000000000..0fa8bc56f8
Binary files /dev/null and b/Resources/Textures/UserInterface/status/bullets/tiny.png differ