CatStation/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
Samuka 3672a6ed62
Custom icon ammo counter (#44368)
* CustomBulletRenderer

* CustomIconStatusControl

* custom ammo counter system

* custom sprite ammo counter comp

* client custom ammo counter system

* need to register the component

* have tiles show in the ammo counter for the tile gun

* comment

* more sprites

* dont try to make 2 controls

* update mini tile sprite

* add horizontal mult

* remove horizontal mult and fix the math

* ensure there is no errors

* replace magical number

* simpler to understand

* remove unecessary stuff

* remove HasComp<CustomSpriteAmmoCounterComponent

* forgot those

* add important stuff back

* explanation

* damn, so simple

* make it better

* use item instead of icon

* remove useless comment

* set number of rows

* fix odd number

* component to hide the name

* update ammo capacity

* set the aligment to vertical

* add 2 px of margin

* move the margin up (that fixes things somehow)

* comment good
2026-07-02 20:31:20 +00:00

387 lines
12 KiB
C#

using System.Numerics;
using Content.Client.IoC;
using Content.Client.Items;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Client.Weapons.Ranged.Components;
using Content.Client.Weapons.Ranged.ItemStatus;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
private void OnAmmoCounterCollect(Entity<AmmoCounterComponent> ent, ref ItemStatusCollectMessage args)
{
RefreshControl(ent);
if (ent.Comp.Control != null)
args.Controls.Add(ent.Comp.Control);
}
/// <summary>
/// Refreshes the control being used to show ammo. Useful if you change the AmmoProvider.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
private void RefreshControl(Entity<AmmoCounterComponent> ent)
{
ent.Comp.Control?.Dispose();
ent.Comp.Control = null;
var ev = new AmmoCounterControlEvent();
RaiseLocalEvent(ent, ev, false);
// Fallback to default if none specified
ev.Control ??= new DefaultStatusControl();
ent.Comp.Control = ev.Control;
UpdateAmmoCount(ent);
}
private void UpdateAmmoCount(Entity<AmmoCounterComponent> ent)
{
if (ent.Comp.Control == null)
return;
var ev = new UpdateAmmoCounterEvent()
{
Control = ent.Comp.Control
};
RaiseLocalEvent(ent, ev, false);
}
protected override void UpdateAmmoCount(EntityUid uid, bool prediction = true)
{
// Don't use resolves because the method is shared and there's no compref and I'm trying to
// share as much code as possible
if (prediction && !Timing.IsFirstTimePredicted ||
!TryComp<AmmoCounterComponent>(uid, out var clientComp))
{
return;
}
UpdateAmmoCount((uid, clientComp));
}
/// <summary>
/// Raised when an ammocounter is requesting a control.
/// </summary>
public sealed class AmmoCounterControlEvent : EntityEventArgs
{
public Control? Control;
}
/// <summary>
/// Raised whenever the ammo count / magazine for a control needs updating.
/// </summary>
public sealed class UpdateAmmoCounterEvent : HandledEntityEventArgs
{
public Control Control = default!;
}
#region Controls
private sealed class DefaultStatusControl : Control
{
private readonly BulletRender _bulletRender;
public DefaultStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(_bulletRender = new BulletRender
{
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom
});
}
public void Update(int count, int capacity)
{
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
_bulletRender.Type = capacity switch
{
> 50 => BulletRender.BulletType.Tiny,
> 15 => BulletRender.BulletType.Normal,
_ => BulletRender.BulletType.Large
};
}
}
public sealed class BoxesStatusControl : Control
{
private readonly BatteryBulletRenderer _bullets;
private readonly Label _ammoCount;
public BoxesStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = Control.VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Children =
{
(_bullets = new BatteryBulletRenderer
{
Margin = new Thickness(0, 0, 5, 0),
HorizontalExpand = true
}),
(_ammoCount = new Label
{
StyleClasses = { StyleClass.ItemStatus },
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom
}),
}
});
}
public void Update(int count, int max)
{
_ammoCount.Visible = true;
_ammoCount.Text = $"x{count:00}";
_bullets.Capacity = max;
_bullets.Count = count;
}
}
public sealed class CustomIconStatusControl : Control
{
private readonly CustomBulletRenderer _customBulletRenderer;
public CustomIconStatusControl(Texture loadedSprite, Texture spentTexture, int numberOfRows)
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(_customBulletRenderer = new CustomBulletRenderer(loadedSprite, spentTexture, numberOfRows)
{
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom,
});
}
public void Update(int count, int capacity)
{
_customBulletRenderer.Capacity = capacity;
_customBulletRenderer.Count = count;
}
}
private sealed class ChamberMagazineStatusControl : Control
{
private readonly BulletRender _bulletRender;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
private readonly Label _ammoCount;
public ChamberMagazineStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = Control.VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
new Control
{
HorizontalExpand = true,
Margin = new Thickness(0, 0, 5, 0),
Children =
{
(_bulletRender = new BulletRender
{
HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom
}),
(_noMagazineLabel = new Label
{
Text = "No Magazine!",
StyleClasses = {StyleClass.ItemStatus}
})
}
},
new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
VerticalAlignment = VAlignment.Bottom,
Margin = new Thickness(0, 0, 0, 2),
Children =
{
(_ammoCount = new Label
{
StyleClasses = {StyleClass.ItemStatus},
HorizontalAlignment = HAlignment.Right,
}),
(_chamberedBullet = new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
HorizontalAlignment = HAlignment.Left,
}),
}
}
}
});
}
public void Update(bool chambered, bool magazine, int count, int capacity)
{
_chamberedBullet.ModulateSelfOverride =
chambered ? Color.FromHex("#d7df60") : Color.Black;
if (!magazine)
{
_bulletRender.Visible = false;
_noMagazineLabel.Visible = true;
_ammoCount.Visible = false;
return;
}
_bulletRender.Visible = true;
_noMagazineLabel.Visible = false;
_ammoCount.Visible = true;
_bulletRender.Count = count;
_bulletRender.Capacity = capacity;
_bulletRender.Type = capacity switch
{
> 50 => BulletRender.BulletType.Tiny,
> 15 => BulletRender.BulletType.Normal,
_ => BulletRender.BulletType.Large
};
_ammoCount.Text = $"x{count:00}";
}
public void PlayAlarmAnimation(Animation animation)
{
_noMagazineLabel.PlayAnimation(animation, "alarm");
}
}
private sealed class RevolverStatusControl : Control
{
private readonly BoxContainer _bulletsList;
public RevolverStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = Control.VAlignment.Center;
AddChild((_bulletsList = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}));
}
public void Update(int currentIndex, bool?[] bullets)
{
_bulletsList.RemoveAllChildren();
var capacity = bullets.Length;
string texturePath;
if (capacity <= 20)
{
texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
}
else if (capacity <= 30)
{
texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
}
else
{
texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
}
var texture = StaticIoC.ResC.GetTexture(texturePath);
var spentTexture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/empty.png");
FillBulletRow(currentIndex, bullets, _bulletsList, texture, spentTexture);
}
private void FillBulletRow(int currentIndex, bool?[] bullets, Control container, Texture texture, Texture emptyTexture)
{
var capacity = bullets.Length;
var colorA = Color.FromHex("#b68f0e");
var colorB = Color.FromHex("#d7df60");
var colorSpentA = Color.FromHex("#b50e25");
var colorSpentB = Color.FromHex("#d3745f");
var colorGoneA = Color.FromHex("#000000");
var colorGoneB = Color.FromHex("#222222");
var altColor = false;
var scale = 1.3f;
for (var i = 0; i < capacity; i++)
{
var bulletFree = bullets[i];
// Add a outline
var box = new Control()
{
MinSize = texture.Size * scale,
};
if (i == currentIndex)
{
box.AddChild(new TextureRect
{
Texture = texture,
TextureScale = new Vector2(scale, scale),
ModulateSelfOverride = Color.LimeGreen,
});
}
Color color;
Texture bulletTexture = texture;
if (bulletFree.HasValue)
{
if (bulletFree.Value)
{
color = altColor ? colorA : colorB;
}
else
{
color = altColor ? colorSpentA : colorSpentB;
bulletTexture = emptyTexture;
}
}
else
{
color = altColor ? colorGoneA : colorGoneB;
}
box.AddChild(new TextureRect
{
Stretch = TextureRect.StretchMode.KeepCentered,
Texture = bulletTexture,
ModulateSelfOverride = color,
});
altColor ^= true;
container.AddChild(box);
}
}
}
#endregion
}