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 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 @@ + + + + + + + + + + + + + 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 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