From 411c23c46eb05635769fd5186f4cc2d3122d23c4 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 9 Jan 2020 00:27:52 +0100 Subject: [PATCH] Item status! --- Content.Client/Content.Client.csproj | 3 + Content.Client/EntryPoint.cs | 4 - .../Components/HandheldLightComponent.cs | 106 +++++++ .../GameObjects/Components/IItemStatus.cs | 33 ++ .../Components/Items/ItemStatusComponent.cs | 12 + .../GameObjects/Components/StackComponent.cs | 60 ++++ .../BallisticMagazineWeaponComponent.cs | 284 ++++++++++++++++++ .../GameObjects/Components/WelderComponent.cs | 74 +++++ Content.Client/StaticIoC.cs | 10 + Content.Client/UserInterface/GameHud.cs | 18 +- Content.Client/UserInterface/HandsGui.cs | 85 ++---- .../UserInterface/ItemStatusPanel.cs | 121 ++++++++ Content.Client/UserInterface/NanoStyle.cs | 44 ++- Content.Client/Utility/RichTextLabelExt.cs | 13 + .../Interactable/HandheldLightComponent.cs | 29 +- .../Interactable/Tools/WelderComponent.cs | 34 ++- .../Components/Stack/StackComponent.cs | 21 +- .../BallisticMagazineWeaponComponent.cs | 80 +++-- .../SharedHandheldLightComponent.cs | 24 ++ .../Components/SharedStackComponent.cs | 26 ++ .../BallisticMagazineWeaponComponentState.cs | 39 +++ .../Components/WelderComponentState.cs | 21 ++ Content.Shared/GameObjects/ContentNetIDs.cs | 4 + Resources/Nano/item_status_left.svg | 100 ++++++ Resources/Nano/item_status_left.svg.96dpi.png | Bin 0 -> 225 bytes Resources/Nano/item_status_right.svg | 105 +++++++ .../Nano/item_status_right.svg.96dpi.png | Bin 0 -> 228 bytes .../Entities/Weapons/Rifles/rifles.yml | 22 +- .../Prototypes/Entities/items/materials.yml | 1 + Resources/Prototypes/Entities/items/tools.yml | 3 +- .../UserInterface/Inventory/hand_l.png | Bin 954 -> 1191 bytes .../UserInterface/Inventory/hand_r.png | Bin 926 -> 1179 bytes .../status/bullets/chambered.png | Bin 0 -> 496 bytes .../UserInterface/status/bullets/normal.png | Bin 0 -> 498 bytes .../UserInterface/status/bullets/small.png | Bin 0 -> 497 bytes .../UserInterface/status/bullets/tiny.png | Bin 0 -> 479 bytes 36 files changed, 1248 insertions(+), 128 deletions(-) create mode 100644 Content.Client/GameObjects/Components/HandheldLightComponent.cs create mode 100644 Content.Client/GameObjects/Components/IItemStatus.cs create mode 100644 Content.Client/GameObjects/Components/Items/ItemStatusComponent.cs create mode 100644 Content.Client/GameObjects/Components/StackComponent.cs create mode 100644 Content.Client/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponent.cs create mode 100644 Content.Client/GameObjects/Components/WelderComponent.cs create mode 100644 Content.Client/StaticIoC.cs create mode 100644 Content.Client/UserInterface/ItemStatusPanel.cs create mode 100644 Content.Client/Utility/RichTextLabelExt.cs create mode 100644 Content.Shared/GameObjects/Components/SharedHandheldLightComponent.cs create mode 100644 Content.Shared/GameObjects/Components/SharedStackComponent.cs create mode 100644 Content.Shared/GameObjects/Components/Weapons/Ranged/BallisticMagazineWeaponComponentState.cs create mode 100644 Content.Shared/GameObjects/Components/WelderComponentState.cs create mode 100644 Resources/Nano/item_status_left.svg create mode 100644 Resources/Nano/item_status_left.svg.96dpi.png create mode 100644 Resources/Nano/item_status_right.svg create mode 100644 Resources/Nano/item_status_right.svg.96dpi.png create mode 100644 Resources/Textures/UserInterface/status/bullets/chambered.png create mode 100644 Resources/Textures/UserInterface/status/bullets/normal.png create mode 100644 Resources/Textures/UserInterface/status/bullets/small.png create mode 100644 Resources/Textures/UserInterface/status/bullets/tiny.png 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 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..9fb2e1748426d2d9227d0927fe9098c3e1adfebb GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPF + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..35e200c21f12576efb7d1db12c6c7c12391890ba GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFzopr07KkDPXGV_ literal 0 HcmV?d00001 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 fef9589a4a58538d396e613fd727c6cc529e7eb0..c760ae8ee838809a152becda25fa4cae673c42a6 100644 GIT binary patch literal 1191 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0V9d*Ob`Ho)PG(@xm{>Z|+MC%? zq;!LSV*IU+jD_Hq4h+}5M-WOuPBJFzsCGGoBxhS??CSYCmAmU8W?)d-kk;i`0CMOWqA~ zRzbUxeSQ3o|195g|3UCqt3_3R_rE`Xan3mls~@vk+)GdEarJW_5AUA#$oFBH?R+op z_uTEdmJ2?#UNY^M7kzG17n-9kJo!Pmt@BR#HGdOq^Q8|pAF6d*xFr1i;yj@q9;eC#kQ)%8V7A&l>S6J zAZ+LVbC*x{@l@Y!4$P=1xI4Wr<4)80uGe)kJKjB4ymI&O%*fB{J^YgM7lmg<-Iv?4 z?(oS<-^<$`mM~krUo&OyuW7GRer=Aduxp!EnX$X+z@q7)rE?cYd#pPt9DC@T9?yw? z*OuzPh)Vy@8MglfFakIWJR*x37`TN%nDNrxxiAkxe>uRX$YHH|d zYU*nNk(QperoJ|i)YjM5HrCbF*VQr5Q!&%iHPqKNG|)3LR5dr$G&fQ;H`6dPGqW)_ zv#~HRvoN={u&}eVw70Ubv$C|cva++*G_$s{vjHM&ds|~yTN?*^6E}NPS9?1L2OTp9 z6E_D_R|hLMM-w+kGdCwwR~K_vS94c)OIJ@zS1&79@4x_GYd8ObX8(d#|H5Yf!d6jH zNuX<0R5a`(a~&e{9Ha6bqw}4j3!Gw#oKo_fQ}dnE3SBaaTtTSVEwjWYug)hQh#Gtf z>wF3ueDfQ93mSck8o>s(1{Aaf6t)ExvEP02UIUIuJF%xA5uZzjCt(h--I_y0YkSkj6P zelrYRbnRMDMiX1q+Zra;s;$wE4wH^ZBrtbO5@Gw~#S>Gd zov11=Da5k#mtJTAk9hCv9>IW!3Zp0{?F|IqrRbv+r?@^plq{u8mLkGoF}bTdH}v({U2dk7d>+>2@ZU zsx1x~9(EL&vGbivyTMtBfI#Vrybt%JZZBD8#Ae7{%len&^rtiN(UULxh`;!<)V@@A zmcj9PmKVA@3_Ap*`o4d){9;pV^Q$4UjE`&IW!dHbb9yo!J>T0jb;m;M4JS`7HQe;q z^N5<~?-v4V!}h!q_!PmJcJdALgzryu7L-=o{#pLUPNCfT>p)hc@2{wsSzdWNUxm%x3%IA!p3^>bP0l+XkKSfd( zmv1xtDiTH-yih1WsZ?R}1S5oi5F+`1e0)SDmLxHvfN9#y_kG};&jnmVa7`z`BLN&q zXqu)l@K&Pq5CPBg<_c~Y8Z19~4JTXgAq;s!;^d?b|0FQ)x3^3kmQnqn(SS}jObDdg zLKU!W9g{$VqJLY0E-sP<{0VKyavU zy~Bzi(<>k+%{DH?5r2kfXp0MQh@X&)q6HxFKRo&p^@29lV3(8O?s*- zkdGIL10i0mM-Qp)#uJJb0fG~DzJ3{NT&JQvOlZ<7Fn{oYC1FE!{~pcft4OH-MlL`Y zG0WpIz{pV$IRSyI zQ4NWN8AbeA6p#d*$iY(9@U41aYBpr+NGc*p<#T04ggk#r&F{OcfpssZ^2Ce^bfYd5 z|5tM&PJc}K$-7kuRtXJ1$4Z=|0%BSPq}V3Gy}ctA4v~1^DAah}IB^w79J#73 zVV1Y^tJI(+CAS1MG^BnjJglYcq01SWPu4}GA9NtNd!W`y5P7t;)V zz_#;P4R!)7N!U&STJ>{iecNPB(isWC#7-!aF~_O$bCm+h`8e3M1J*9zrLg3INiIm( zj*DX=aNHtpNZLa#Ikf@-bihUT3v&$Z7>(&Xg0mloVD25V_WFh*p@WIf3uxASX!iZU zpMTW})72312%2?YDPSDzGI9%5>0*{QLTCo3YApS^!&*Hwup(8CIFl2OY_a@L%#eUq zY%&9BjqRH1kK_WCxi>P;EIT5Vf4CS~!W^=KL4UmBS>yK!*R>-WM_{Z1>Ma&TVgP=r z0{i=WblwM(Y?B9*^onYvk4y5zg6tN0JSdakgHzFn0_hvq)r`R@^`?&Je*th}53D>B Rk(mGh002ovPDHLkV1f|!s2TtO diff --git a/Resources/Textures/UserInterface/Inventory/hand_r.png b/Resources/Textures/UserInterface/Inventory/hand_r.png index ae692d32b1bdb71554be9073152293686f448843..b3385441318dcf99d7fb2276a30d3e4b0c8094b4 100644 GIT binary patch literal 1179 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0V9d*Ob`Ho)PG(@xm{>Z|+MC%? zq;!LSV*IU+jD_Hq4h+}5M-WOuPBJFzsCGGoBxhS??CSYCmAmU8W?)d-kk;i`0CMOWqA~ zRzbUxeSQ3o|195g|3UCqt3_3R_rE`Xan3mls~@vk+)GdEarJW_5AUA#$oFBH?R+op z_uTEdmJ2?#UNY^M7kzG17n-9kJo!Pmt@BR#HGdOq^Q8|pAF6d*xFr1i;yj@q9;eC#kQ)%8V7A&l>S6J zAZ+LVbC*x{@l@Y!4$P=1xI4Wr<4)80uGe)kJKjB4ymI&O%*fB{J^YgM7lmg<-Iv?4 z?(oS<-^<$`mM~krUo&OyuW7GRer=Aduxp!EnX$X+z@q7)rE?cYd#pPt9DC@T9?yw? z*OuzPh)Vy@8MglfFakIWJR*x37`TN%nDNrxxiAkxe>uLazhMp!6 zY3gff>1k`~Yik*3Ya8om>+5J6>gX8gYU}Ij80hI5>gyUB=ouNRnj31Gn`xMtnc0{F zk-4q8rG zsjI!6gM*HlgNd7ysjG{*t1A%My1HAsdRn@AS-E-#2KZXL`4=?%7qt2pHv1R01{Aaf z6t;2$Bqsh;Q+D8?C->BMyK|9{2a1(P3L zShxOr?Ygas$Z^rk-(Bw9MAG%oCJ)jY;K z;kKl8#Gxg;^~;$WN)BIOSfIvh|KaK8=#+iG_q{J$Z_${*a6+b%@mR5Gm!D+C!wvV2 zehhapxbQmjun=ocpuqFM##8=M73z4*|V>8pXaRRFMk?8FRZ-&x%P38f{&2GY1vuN-A}xIt->5X zEl=Q8jLdbDD-$a|=|xOa-N$|+)!~NPPuGJytba_m%;1T7^?%>JJ?E|4^E)}RP6s^L RJOLPk44$rjF6*2Ung9St7ViK6 delta 915 zcmV;E18n@837!X#8Gi-<0047(dh`GQ010qNS#tmY3labT3lag+-G2N400U4-L_t(o z!^KxoZ_-c{J}qr`3zU*=u+10ed=e9lWG0%-#Fr($_)FC%pUh13MWYX!CMH{=A?{5S zFl~o$3+=t0d%Lm@#s;How;`0a-0!>JIrlp~3g9T0Z?xf>hJO-+mr85bLO@G`HyXdl zRe&NMJw+e}FBA%}yj-S=qJVkG8q_CQS#2*MB;ONWT6 zsuBZF$%q1C6pKaBG72niZXag|fU~;;y?zK`n2r*HM61<+GYNRE3%aJl5-QO8x&uM4 z2jz#)ptkc8m4D!=sW6g+@vvf|nDRSUeN3crWh2628^6DN zk|yKw?)1B0903vOj4+aL0dkld-SrAl^D3CcLhm?naGvL&ZapHIZwC~(A#)^|6Ei6w zR=rStKzsx)WS2qsr81m^h-FzT;BEkM_ukR72u!Sim|6gc|5)gr{kwOFl`o$F^JBkG zDuver)_;>g>x--4GJ1IhG<-O~-(uy#lPa-tWt3DMtn{LB__sc&DN`#D|FMAJ;q3~^ zZNEZ2sm}k{m8`%*PDANO6(N`R|2427xLK>)`i}&LdL1s zi_s5#=rld(;7&+w7lh0Q-)~3Y7+>lm5wkS7FMqBHGQvBk??LWxpN9D&q8YRKfV5d$ ziMqK(vZX=h z^nVnH9guxLpqkFXyHz1k#lxCGil7)2GPQk7I4&(&TsJZP6tmw3BmG*CZF++@+D>wd z?bT^2Z$y(8Bl$cYslE|SQw`_#PAKC$ClWFESOsdeA4D`J^He}ICdmq_CEF#@u^_v- pwr2wT%+_qUXS=fQ_`2Kk{~R9NfqKD7>I?t?002ovPDHLkV1oQ6rVs!C diff --git a/Resources/Textures/UserInterface/status/bullets/chambered.png b/Resources/Textures/UserInterface/status/bullets/chambered.png new file mode 100644 index 0000000000000000000000000000000000000000..02d6bb406aa7a591d523b6585746acb8dc449705 GIT binary patch literal 496 zcmV1DYb%Cm*h5Sg(M9(DN3jU3WQL4txRHlf2ooU%zX>+VZ5eM_j2htc0c4V)Y$l*t<-wMt!8${Oh^eL8Al z?*Hd$Ysnd}dpHI3zq;<{BrvcKH0rMVd+NH4Ga&E+T$^owh2}K(O?thpMUR2V4sdzX z)|3O_Y8M!Nwq!0hr2s9zrU4%pz)%4gz6S=^y;-&PasCA4x9N%QfWsqTis~a9f8#c1 z{qo-e1ONa432;bRa{vGjVE_ORVF9Q=r)dBH03}I8K~xCWWBd;U3aX`~;Gm}gRE4fu6 z_zoX}%rOjzvSrMt(-YC^Frz)%}b%jp}6?gCd z_i3rqQ{MDY0_c8qoX>F}xDV8-j`Mr!IJGk%@B&=xP5&nMH2Y1x+SDS)KzIkZylHCE z0dTbo3_ly9Db}R`)32()#|6-z0|xJb-ZgJmjdPqo0oiT-qC4R52$*1hc>NE5s%OtB z0@m{Y000SaNLh0L01sgR01sgSs6VG^0000fNklPx# literal 0 HcmV?d00001 diff --git a/Resources/Textures/UserInterface/status/bullets/small.png b/Resources/Textures/UserInterface/status/bullets/small.png new file mode 100644 index 0000000000000000000000000000000000000000..66215c85e3705110ede8a7b260ea4ed41c3ea880 GIT binary patch literal 497 zcmV1DYb%Cm*h5Sg(M9(DN3jU3WQL4txRHlf2ooU%zX>+VZ5eM_j2htc0c4V)Y$l*t<-wMt!8${Oh^eL8Al z?*Hd$Ysnd}dpHI3zq;<{BrvcKH0rMVd+NH4Ga&E+T$^owh2}K(O?thpMUR2V4sdzX z)|3O_Y8M!Nwq!0hr2s9zrU4%pz)%4gz6S=^y;-&PasCA4x9N%QfWsqTis~a9f8#c1 z{qo-e1ONa432;bRa{vGjVE_ORVF9Q=r)dBH047O9K~xCWV_*P+|Ns9tfG~*72xNf8 n7$AU=35f%iU}8W6gGB%Uc~K1u`%Pl^00000NkvXXu0mjf0y)lF literal 0 HcmV?d00001 diff --git a/Resources/Textures/UserInterface/status/bullets/tiny.png b/Resources/Textures/UserInterface/status/bullets/tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..0fa8bc56f8bf786fa8e7fe910ac93343cb6b2f2e GIT binary patch literal 479 zcmV<50U-W~P)1DYb%Cm*h5Sg(M9(DN3jU3WQL4txRHlf2ooU%zX>+VZ5eM_j2htc0c4V)Y$l*t<-wMt!8${Oh^eL8Al z?*Hd$Ysnd}dpHI3zq;<{BrvcKH0rMVd+NH4Ga&E+T$^owh2}K(O?thpMUR2V4sdzX z)|3O_Y8M!Nwq!0hr2s9zrU4%pz)%4gz6S=^y;-&PasCA4x9N%QfWsqTis~a9f8#c1 z{qo-e1ONa432;bRa{vGjVE_ORVF9Q=r)dBH02E0?K~xCWWBd;U3_!rd00p#T0061w V1Qcgk{FeX#002ovPDHLkV1k=h$5;RW literal 0 HcmV?d00001