diff --git a/Content.Client/GameObjects/Components/Weapons/Ranged/Barrels/ClientBatteryBarrelComponent.cs b/Content.Client/GameObjects/Components/Weapons/Ranged/Barrels/ClientBatteryBarrelComponent.cs new file mode 100644 index 0000000000..9de6ba9b71 --- /dev/null +++ b/Content.Client/GameObjects/Components/Weapons/Ranged/Barrels/ClientBatteryBarrelComponent.cs @@ -0,0 +1,160 @@ +using Content.Client.UserInterface.Stylesheets; +using Content.Shared.GameObjects; +using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels; +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.ViewVariables; +using System; + +namespace Content.Client.GameObjects.Components.Weapons.Ranged.Barrels +{ + [RegisterComponent] + public class ClientBatteryBarrelComponent : Component, IItemStatus + { + public override string Name => "BatteryBarrel"; + public override uint? NetID => ContentNetIDs.BATTERY_BARREL; + + private StatusControl _statusControl; + + /// + /// Count of bullets in the magazine. + /// + /// + /// Null if no magazine is inserted. + /// + [ViewVariables] + public (int count, int max)? MagazineCount { get; private set; } + + public override void HandleComponentState(ComponentState curState, ComponentState nextState) + { + if (!(curState is BatteryBarrelComponentState cast)) + return; + + MagazineCount = cast.Magazine; + _statusControl?.Update(); + } + + 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 ClientBatteryBarrelComponent _parent; + private readonly HBoxContainer _bulletsList; + private readonly Label _noBatteryLabel; + private readonly Label _ammoCount; + + public StatusControl(ClientBatteryBarrelComponent parent) + { + _parent = parent; + SizeFlagsHorizontal = SizeFlags.FillExpand; + SizeFlagsVertical = SizeFlags.ShrinkCenter; + + AddChild(new HBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Control + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + (_bulletsList = new HBoxContainer + { + SizeFlagsVertical = SizeFlags.ShrinkCenter, + SeparationOverride = 4 + }), + (_noBatteryLabel = new Label + { + Text = "No Battery!", + StyleClasses = {StyleNano.StyleClassItemStatus} + }) + } + }, + new Control() { CustomMinimumSize = (5,0) }, + (_ammoCount = new Label + { + StyleClasses = {StyleNano.StyleClassItemStatus}, + SizeFlagsHorizontal = SizeFlags.ShrinkEnd, + }), + } + }); + } + + public void Update() + { + _bulletsList.RemoveAllChildren(); + + if (_parent.MagazineCount == null) + { + _noBatteryLabel.Visible = true; + _ammoCount.Visible = false; + return; + } + + var (count, capacity) = _parent.MagazineCount.Value; + + _noBatteryLabel.Visible = false; + _ammoCount.Visible = true; + + _ammoCount.Text = $"x{count:00}"; + capacity = Math.Min(capacity, 8); + FillBulletRow(_bulletsList, count, capacity); + } + + private static void FillBulletRow(Control container, int count, int capacity) + { + var colorGone = Color.FromHex("#000000"); + var color = Color.FromHex("#E00000"); + + // Draw the empty ones + for (var i = count; i < capacity; i++) + { + container.AddChild(new PanelContainer + { + PanelOverride = new StyleBoxFlat() + { + BackgroundColor = colorGone, + }, + CustomMinimumSize = (10, 15), + }); + } + + // Draw the full ones, but limit the count to the capacity + count = Math.Min(count, capacity); + for (var i = 0; i < count; i++) + { + container.AddChild(new PanelContainer + { + PanelOverride = new StyleBoxFlat() + { + BackgroundColor = color, + }, + CustomMinimumSize = (10, 15), + }); + } + } + + protected override Vector2 CalculateMinimumSize() + { + return Vector2.ComponentMax((0, 15), base.CalculateMinimumSize()); + } + } + } +} diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 9a72a27e22..28545a1909 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -105,7 +105,6 @@ "SecureEntityStorage", "PresetIdCard", "SolarControlConsole", - "BatteryBarrel", "FlashExplosive", "FlashProjectile", "Utensil", diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerBatteryBarrelComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerBatteryBarrelComponent.cs index 486d2071b1..d81aa8115e 100644 --- a/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerBatteryBarrelComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerBatteryBarrelComponent.cs @@ -6,7 +6,10 @@ using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Power; using Content.Server.GameObjects.Components.Projectiles; using Content.Shared.Damage; +using Content.Shared.GameObjects; using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.GameObjects.Verbs; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Server.GameObjects; using Robust.Server.GameObjects.Components.Container; @@ -25,6 +28,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels public sealed class ServerBatteryBarrelComponent : ServerRangedBarrelComponent { public override string Name => "BatteryBarrel"; + public override uint? NetID => ContentNetIDs.BATTERY_BARREL; // The minimum change we need before we can fire [ViewVariables] private float _lowerChargeLimit; @@ -88,6 +92,15 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels serializer.DataField(ref _soundPowerCellEject, "soundPowerCellEject", null); } + public override ComponentState GetComponentState() + { + (int, int)? count = (ShotsLeft, Capacity); + + return new BatteryBarrelComponentState( + FireRateSelector, + count); + } + public override void Initialize() { base.Initialize(); @@ -108,6 +121,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels _appearanceComponent = appearanceComponent; } + Dirty(); UpdateAppearance(); } @@ -188,8 +202,8 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?"); } + Dirty(); UpdateAppearance(); - //Dirty(); return entity; } @@ -211,30 +225,12 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels } _powerCellContainer.Insert(entity); + + Dirty(); UpdateAppearance(); - //Dirty(); return true; } - private IEntity RemovePowerCell() - { - if (!_powerCellRemovable || _powerCellContainer.ContainedEntity == null) - { - return null; - } - - var entity = _powerCellContainer.ContainedEntity; - _powerCellContainer.Remove(entity); - if (_soundPowerCellEject != null) - { - EntitySystem.Get().PlayAtCoords(_soundPowerCellEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2)); - } - - UpdateAppearance(); - //Dirty(); - return entity; - } - public override bool UseEntity(UseEntityEventArgs eventArgs) { if (!_powerCellRemovable) @@ -242,22 +238,44 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels return false; } - if (!eventArgs.User.TryGetComponent(out HandsComponent handsComponent) || - PowerCellEntity == null) + if (PowerCellEntity == null) { return false; } - var itemComponent = PowerCellEntity.GetComponent(); - if (!handsComponent.CanPutInHand(itemComponent)) + return TryEjectCell(eventArgs.User); + } + + private bool TryEjectCell(IEntity user) + { + if (PowerCell == null || !_powerCellRemovable) { return false; } - var powerCell = RemovePowerCell(); - handsComponent.PutInHand(itemComponent); - powerCell.Transform.GridPosition = eventArgs.User.Transform.GridPosition; + if (!user.TryGetComponent(out HandsComponent hands)) + { + return false; + } + var cell = PowerCell; + if (!_powerCellContainer.Remove(cell.Owner)) + { + return false; + } + + Dirty(); + UpdateAppearance(); + + if (!hands.PutInHand(cell.Owner.GetComponent())) + { + cell.Owner.Transform.GridPosition = user.Transform.GridPosition; + } + + if (_soundPowerCellEject != null) + { + EntitySystem.Get().PlayAtCoords(_soundPowerCellEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2)); + } return true; } @@ -270,5 +288,33 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels return TryInsertPowerCell(eventArgs.Using); } + + [Verb] + public sealed class EjectCellVerb : Verb + { + protected override void GetData(IEntity user, ServerBatteryBarrelComponent component, VerbData data) + { + if (!ActionBlockerSystem.CanInteract(user) || !component._powerCellRemovable) + { + data.Visibility = VerbVisibility.Invisible; + return; + } + + if (component.PowerCell == null) + { + data.Text = "Eject cell (cell missing)"; + data.Visibility = VerbVisibility.Disabled; + } + else + { + data.Text = "Eject cell"; + } + } + + protected override void Activate(IEntity user, ServerBatteryBarrelComponent component) + { + component.TryEjectCell(user); + } + } } } diff --git a/Content.Shared/GameObjects/Components/Weapons/Ranged/Barrels/SharedBatteryBarrelComponent.cs b/Content.Shared/GameObjects/Components/Weapons/Ranged/Barrels/SharedBatteryBarrelComponent.cs new file mode 100644 index 0000000000..40ef9b47e1 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Weapons/Ranged/Barrels/SharedBatteryBarrelComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels +{ + [Serializable, NetSerializable] + public class BatteryBarrelComponentState : ComponentState + { + public FireRateSelector FireRateSelector { get; } + public (int count, int max)? Magazine { get; } + + public BatteryBarrelComponentState( + FireRateSelector fireRateSelector, + (int count, int max)? magazine) : + base(ContentNetIDs.BATTERY_BARREL) + { + FireRateSelector = fireRateSelector; + Magazine = magazine; + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index f375525110..32ede76f3d 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -70,6 +70,7 @@ public const uint REVOLVER_BARREL = 1064; public const uint CUFFED = 1065; public const uint HANDCUFFS = 1066; + public const uint BATTERY_BARREL = 1067; // Net IDs for integration tests. public const uint PREDICTION_TEST = 10001; diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml index ef5ae605f1..f57ce3d57f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml @@ -30,6 +30,7 @@ - Single fireRate: 2 powerCellPrototype: PowerCellSmallStandard + powerCellRemovable: true ammoPrototype: RedLaser soundGunshot: /Audio/Weapons/Guns/Gunshots/laser.ogg - type: Appearance @@ -71,6 +72,7 @@ angleIncrease: 15 angleDecay: 45 powerCellPrototype: PowerCellSmallSuper + powerCellRemovable: true ammoPrototype: RedHeavyLaser soundGunshot: /Audio/Weapons/Guns/Gunshots/laser_cannon.ogg - type: Appearance @@ -112,6 +114,7 @@ angleIncrease: 15 angleDecay: 45 powerCellPrototype: PowerCellSmallSuper + powerCellRemovable: true base_fire_cost: 600 ammoPrototype: XrayLaser soundGunshot: /Audio/Weapons/Guns/Gunshots/laser3.ogg @@ -155,6 +158,7 @@ angleIncrease: 20 angleDecay: 15 powerCellPrototype: PowerCellSmallStandard + powerCellRemovable: false ammoPrototype: BulletTaser soundGunshot: /Audio/Weapons/Guns/Gunshots/taser.ogg - type: Appearance