Gun refactor (#8301)

Co-authored-by: Kara <lunarautomaton6@gmail.com>
Co-authored-by: T-Stalker <le0nel_1van@hotmail.com>
Co-authored-by: T-Stalker <43253663+DogZeroX@users.noreply.github.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-06-01 19:59:58 +10:00
committed by GitHub
parent 1ced3c5002
commit fb943a61dc
1051 changed files with 8230 additions and 99090 deletions

View File

@@ -23,7 +23,6 @@
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.Analyzers.targets" />
</Project>

View File

@@ -0,0 +1,17 @@
using Robust.Client.Animations;
using Robust.Client.GameObjects;
namespace Content.Client.Effects;
public sealed class EffectVisualizerSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<EffectVisualsComponent, AnimationCompletedEvent>(OnEffectAnimComplete);
}
private void OnEffectAnimComplete(EntityUid uid, EffectVisualsComponent component, AnimationCompletedEvent args)
{
QueueDel(uid);
}
}

View File

@@ -0,0 +1,8 @@
namespace Content.Client.Effects;
[RegisterComponent]
public sealed class EffectVisualsComponent : Component
{
public float Length;
public float Accumulator = 0f;
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.Items
{

View File

@@ -1,140 +0,0 @@
using Content.Client.Items.Components;
using Content.Client.Stylesheets;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameStates;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Weapons.Ranged.Barrels.Components
{
[RegisterComponent]
[NetworkedComponent()]
public sealed class ClientBatteryBarrelComponent : Component, IItemStatus
{
public StatusControl? ItemStatus;
public Control MakeControl()
{
ItemStatus = new StatusControl(this);
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(Owner, out AppearanceComponent appearance))
ItemStatus.Update(appearance);
return ItemStatus;
}
public void DestroyControl(Control control)
{
if (ItemStatus == control)
{
ItemStatus = null;
}
}
public sealed class StatusControl : Control
{
private readonly ClientBatteryBarrelComponent _parent;
private readonly BoxContainer _bulletsList;
private readonly Label _noBatteryLabel;
private readonly Label _ammoCount;
public StatusControl(ClientBatteryBarrelComponent parent)
{
MinHeight = 15;
_parent = parent;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsList = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 4
}),
(_noBatteryLabel = new Label
{
Text = "No Battery!",
StyleClasses = {StyleNano.StyleClassItemStatus}
})
}
},
new Control() { MinSize = (5,0) },
(_ammoCount = new Label
{
StyleClasses = {StyleNano.StyleClassItemStatus},
HorizontalAlignment = HAlignment.Right,
}),
}
});
}
public void Update(AppearanceComponent appearance)
{
_bulletsList.RemoveAllChildren();
if (!appearance.TryGetData(MagazineBarrelVisuals.MagLoaded, out bool loaded) || !loaded)
{
_noBatteryLabel.Visible = true;
_ammoCount.Visible = false;
return;
}
appearance.TryGetData(AmmoVisuals.AmmoCount, out int count);
appearance.TryGetData(AmmoVisuals.AmmoMax, out int capacity);
_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,
},
MinSize = (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,
},
MinSize = (10, 15),
});
}
}
}
}
}

View File

@@ -1,212 +0,0 @@
using System;
using Content.Client.IoC;
using Content.Client.Items.Components;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Weapons.Ranged.Barrels.Components
{
[RegisterComponent]
[NetworkedComponent()]
public sealed class ClientBoltActionBarrelComponent : Component, IItemStatus
{
private StatusControl? _statusControl;
/// <summary>
/// chambered is true when a bullet is chambered
/// spent is true when the chambered bullet is spent
/// </summary>
[ViewVariables]
public (bool chambered, bool spent) Chamber { get; private set; }
/// <summary>
/// Count of bullets in the magazine.
/// </summary>
/// <remarks>
/// Null if no magazine is inserted.
/// </remarks>
[ViewVariables]
public (int count, int max)? MagazineCount { get; private set; }
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not BoltActionBarrelComponentState cast)
return;
Chamber = cast.Chamber;
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 ClientBoltActionBarrelComponent _parent;
private readonly BoxContainer _bulletsListTop;
private readonly BoxContainer _bulletsListBottom;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
public StatusControl(ClientBoltActionBarrelComponent parent)
{
MinHeight = 15;
_parent = parent;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0,
Children =
{
(_bulletsListTop = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 0
}),
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsListBottom = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}),
(_noMagazineLabel = new Label
{
Text = "No Magazine!",
StyleClasses = {StyleNano.StyleClassItemStatus}
})
}
},
(_chamberedBullet = new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Right,
})
}
}
}
});
}
public void Update()
{
_chamberedBullet.ModulateSelfOverride =
_parent.Chamber.chambered ?
_parent.Chamber.spent ? Color.Red : 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/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);
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;
}
}
}
}
}

View File

@@ -1,248 +0,0 @@
using System;
using Content.Client.IoC;
using Content.Client.Items.Components;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Weapons.Ranged.Barrels.Components;
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.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Weapons.Ranged.Barrels.Components
{
[RegisterComponent]
[NetworkedComponent()]
public sealed class ClientMagazineBarrelComponent : Component, IItemStatus
{
private static readonly Animation AlarmAnimationSmg = new()
{
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()
{
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),
}
}
}
};
private StatusControl? _statusControl;
/// <summary>
/// True if a bullet is chambered.
/// </summary>
[ViewVariables]
public bool Chambered { get; private set; }
/// <summary>
/// Count of bullets in the magazine.
/// </summary>
/// <remarks>
/// Null if no magazine is inserted.
/// </remarks>
[ViewVariables]
public (int count, int max)? MagazineCount { get; private set; }
[ViewVariables(VVAccess.ReadWrite)] [DataField("lmg_alarm_animation")] private bool _isLmgAlarmAnimation = default;
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not MagazineBarrelComponentState cast)
return;
Chambered = cast.Chambered;
MagazineCount = cast.Magazine;
_statusControl?.Update();
}
public void PlayAlarmAnimation()
{
_statusControl?.PlayAlarmAnimation();
}
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 ClientMagazineBarrelComponent _parent;
private readonly BoxContainer _bulletsList;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
private readonly Label _ammoCount;
public StatusControl(ClientMagazineBarrelComponent parent)
{
MinHeight = 15;
_parent = parent;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
(_chamberedBullet = new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered_rotated.png"),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Right,
}),
new Control() { MinSize = (5,0) },
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsList = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}),
(_noMagazineLabel = new Label
{
Text = "No Magazine!",
StyleClasses = {StyleNano.StyleClassItemStatus}
})
}
},
new Control() { MinSize = (5,0) },
(_ammoCount = new Label
{
StyleClasses = {StyleNano.StyleClassItemStatus},
HorizontalAlignment = HAlignment.Right,
}),
}
});
}
public void Update()
{
_chamberedBullet.ModulateSelfOverride =
_parent.Chambered ? Color.FromHex("#d7df60") : Color.Black;
_bulletsList.RemoveAllChildren();
if (_parent.MagazineCount == null)
{
_noMagazineLabel.Visible = true;
_ammoCount.Visible = false;
return;
}
var (count, capacity) = _parent.MagazineCount.Value;
_noMagazineLabel.Visible = false;
_ammoCount.Visible = true;
var texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
var texture = StaticIoC.ResC.GetTexture(texturePath);
_ammoCount.Text = $"x{count:00}";
capacity = Math.Min(capacity, 20);
FillBulletRow(_bulletsList, 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;
// Draw the empty ones
for (var i = count; i < capacity; i++)
{
container.AddChild(new TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorGoneA : colorGoneB,
Stretch = TextureRect.StretchMode.KeepCentered
});
altColor ^= true;
}
// 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 TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorA : colorB,
Stretch = TextureRect.StretchMode.KeepCentered
});
altColor ^= true;
}
}
public void PlayAlarmAnimation()
{
var animation = _parent._isLmgAlarmAnimation ? AlarmAnimationLmg : AlarmAnimationSmg;
_noMagazineLabel.PlayAnimation(animation, "alarm");
}
}
}
}

View File

@@ -1,212 +0,0 @@
using System;
using Content.Client.IoC;
using Content.Client.Items.Components;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Weapons.Ranged.Barrels.Components
{
[RegisterComponent]
[NetworkedComponent()]
public sealed class ClientPumpBarrelComponent : Component, IItemStatus
{
private StatusControl? _statusControl;
/// <summary>
/// chambered is true when a bullet is chambered
/// spent is true when the chambered bullet is spent
/// </summary>
[ViewVariables]
public (bool chambered, bool spent) Chamber { get; private set; }
/// <summary>
/// Count of bullets in the magazine.
/// </summary>
/// <remarks>
/// Null if no magazine is inserted.
/// </remarks>
[ViewVariables]
public (int count, int max)? MagazineCount { get; private set; }
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not PumpBarrelComponentState cast)
return;
Chamber = cast.Chamber;
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 ClientPumpBarrelComponent _parent;
private readonly BoxContainer _bulletsListTop;
private readonly BoxContainer _bulletsListBottom;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
public StatusControl(ClientPumpBarrelComponent parent)
{
MinHeight = 15;
_parent = parent;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0,
Children =
{
(_bulletsListTop = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 0
}),
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsListBottom = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}),
(_noMagazineLabel = new Label
{
Text = "No Magazine!",
StyleClasses = {StyleNano.StyleClassItemStatus}
})
}
},
(_chamberedBullet = new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Right,
})
}
}
}
});
}
public void Update()
{
_chamberedBullet.ModulateSelfOverride =
_parent.Chamber.chambered ?
_parent.Chamber.spent ? Color.Red : 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/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);
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;
}
}
}
}
}

View File

@@ -1,168 +0,0 @@
using Content.Client.IoC;
using Content.Client.Items.Components;
using Content.Client.Resources;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Weapons.Ranged.Barrels.Components
{
[RegisterComponent]
[NetworkedComponent()]
public sealed class ClientRevolverBarrelComponent : Component, IItemStatus
{
private StatusControl? _statusControl;
/// <summary>
/// A array that lists the bullet states
/// true means a spent bullet
/// false means a "shootable" bullet
/// null means no bullet
/// </summary>
[ViewVariables]
public bool?[] Bullets { get; private set; } = new bool?[0];
[ViewVariables]
public int CurrentSlot { get; private set; }
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not RevolverBarrelComponentState cast)
return;
CurrentSlot = cast.CurrentSlot;
Bullets = cast.Bullets;
_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 ClientRevolverBarrelComponent _parent;
private readonly BoxContainer _bulletsList;
public StatusControl(ClientRevolverBarrelComponent parent)
{
MinHeight = 15;
_parent = parent;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild((_bulletsList = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}));
}
public void Update()
{
_bulletsList.RemoveAllChildren();
var capacity = _parent.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(_bulletsList, texture, spentTexture);
}
private void FillBulletRow(Control container, Texture texture, Texture emptyTexture)
{
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 < _parent.Bullets.Length; i++)
{
var bulletSpent = _parent.Bullets[i];
// Add a outline
var box = new Control()
{
MinSize = texture.Size * scale,
};
if (i == _parent.CurrentSlot)
{
box.AddChild(new TextureRect
{
Texture = texture,
TextureScale = (scale, scale),
ModulateSelfOverride = Color.LimeGreen,
});
}
Color color;
Texture bulletTexture = texture;
if (bulletSpent.HasValue)
{
if (bulletSpent.Value)
{
color = altColor ? colorSpentA : colorSpentB;
bulletTexture = emptyTexture;
}
else
{
color = altColor ? colorA : colorB;
}
}
else
{
color = altColor ? colorGoneA : colorGoneB;
}
box.AddChild(new TextureRect
{
Stretch = TextureRect.StretchMode.KeepCentered,
Texture = bulletTexture,
ModulateSelfOverride = color,
});
altColor ^= true;
container.AddChild(box);
}
}
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Client.Weapons.Ranged.Barrels.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Weapons.Ranged.Barrels.EntitySystems;
public sealed class ClientBatteryBarrelSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ClientBatteryBarrelComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(EntityUid uid, ClientBatteryBarrelComponent component, ref AppearanceChangeEvent args)
{
component.ItemStatus?.Update(args.Component);
}
}

View File

@@ -1,39 +0,0 @@
using Content.Shared.Weapons.Ranged.Barrels.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Client.Weapons.Ranged.Barrels.Visualizers
{
[UsedImplicitly]
public sealed class BarrelBoltVisualizer : AppearanceVisualizer
{
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(entity);
sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-open");
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
if (!component.TryGetData(BarrelBoltVisuals.BoltOpen, out bool boltOpen))
{
return;
}
if (boltOpen)
{
sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-open");
}
else
{
sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-closed");
}
}
}
}

View File

@@ -1,106 +0,0 @@
using Content.Shared.Rounding;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Weapons.Ranged.Barrels.Visualizers
{
[UsedImplicitly]
public sealed class MagVisualizer : AppearanceVisualizer
{
private bool _magLoaded;
[DataField("magState")]
private string? _magState;
[DataField("steps")]
private int _magSteps;
[DataField("zeroVisible")]
private bool _zeroVisible;
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(entity);
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
{
sprite.LayerSetState(RangedBarrelVisualLayers.Mag, $"{_magState}-{_magSteps-1}");
sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, false);
}
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetState(RangedBarrelVisualLayers.MagUnshaded, $"{_magState}-unshaded-{_magSteps-1}");
sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, false);
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
// tl;dr
// 1.If no mag then hide it OR
// 2. If step 0 isn't visible then hide it (mag or unshaded)
// 3. Otherwise just do mag / unshaded as is
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
component.TryGetData(MagazineBarrelVisuals.MagLoaded, out _magLoaded);
if (_magLoaded)
{
if (!component.TryGetData(AmmoVisuals.AmmoMax, out int capacity))
{
return;
}
if (!component.TryGetData(AmmoVisuals.AmmoCount, out int current))
{
return;
}
var step = ContentHelpers.RoundToLevels(current, capacity, _magSteps);
if (step == 0 && !_zeroVisible)
{
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
{
sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, false);
}
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, false);
}
return;
}
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
{
sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, true);
sprite.LayerSetState(RangedBarrelVisualLayers.Mag, $"{_magState}-{step}");
}
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, true);
sprite.LayerSetState(RangedBarrelVisualLayers.MagUnshaded, $"{_magState}-unshaded-{step}");
}
}
else
{
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.Mag, out _))
{
sprite.LayerSetVisible(RangedBarrelVisualLayers.Mag, false);
}
if (sprite.LayerMapTryGet(RangedBarrelVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetVisible(RangedBarrelVisualLayers.MagUnshaded, false);
}
}
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Shared.Weapons.Ranged.Barrels.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Client.Weapons.Ranged.Barrels.Visualizers
{
[UsedImplicitly]
public sealed class SpentAmmoVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
if (!component.TryGetData(AmmoVisuals.Spent, out bool spent))
{
return;
}
sprite.LayerSetState(AmmoVisualLayers.Base, spent ? "spent" : "base");
}
}
public enum AmmoVisualLayers : byte
{
Base,
}
}

View File

@@ -1,34 +0,0 @@
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.Client.Weapons.Ranged
{
// Yeah I put it all in the same enum, don't judge me
public enum RangedBarrelVisualLayers : byte
{
Base,
BaseUnshaded,
Bolt,
Mag,
MagUnshaded,
}
[RegisterComponent]
public sealed class ClientRangedWeaponComponent : SharedRangedWeaponComponent
{
public FireRateSelector FireRateSelector { get; private set; } = FireRateSelector.Automatic;
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not RangedWeaponComponentState rangedState)
{
return;
}
FireRateSelector = rangedState.FireRateSelector;
}
}
}

View File

@@ -1,3 +1,4 @@
using Content.Client.Weapons.Ranged.Systems;
using Robust.Shared.Console;
namespace Content.Client.Weapons.Ranged;

View File

@@ -0,0 +1,10 @@
using Content.Shared.Weapons.Ranged.Components;
using Robust.Client.UserInterface;
namespace Content.Client.Weapons.Ranged.Components;
[RegisterComponent]
public sealed class AmmoCounterComponent : SharedAmmoCounterComponent
{
public Control? Control;
}

View File

@@ -0,0 +1,107 @@
using Content.Shared.Rounding;
using Content.Shared.Weapons.Ranged.Systems;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
namespace Content.Client.Weapons.Ranged.Components;
[UsedImplicitly]
public sealed class MagVisualizer : AppearanceVisualizer
{
[DataField("magState")] private string? _magState;
[DataField("steps")] private int _magSteps;
[DataField("zeroVisible")] private bool _zeroVisible;
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(entity);
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
{
sprite.LayerSetState(GunVisualLayers.Mag, $"{_magState}-{_magSteps - 1}");
sprite.LayerSetVisible(GunVisualLayers.Mag, false);
}
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetState(GunVisualLayers.MagUnshaded, $"{_magState}-unshaded-{_magSteps - 1}");
sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, false);
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
// tl;dr
// 1.If no mag then hide it OR
// 2. If step 0 isn't visible then hide it (mag or unshaded)
// 3. Otherwise just do mag / unshaded as is
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
if (!component.TryGetData(AmmoVisuals.MagLoaded, out bool magloaded) ||
magloaded)
{
if (!component.TryGetData(AmmoVisuals.AmmoMax, out int capacity))
{
capacity = _magSteps;
}
if (!component.TryGetData(AmmoVisuals.AmmoCount, out int current))
{
current = _magSteps;
}
var step = ContentHelpers.RoundToLevels(current, capacity, _magSteps);
if (step == 0 && !_zeroVisible)
{
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
{
sprite.LayerSetVisible(GunVisualLayers.Mag, false);
}
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, false);
}
return;
}
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
{
sprite.LayerSetVisible(GunVisualLayers.Mag, true);
sprite.LayerSetState(GunVisualLayers.Mag, $"{_magState}-{step}");
}
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, true);
sprite.LayerSetState(GunVisualLayers.MagUnshaded, $"{_magState}-unshaded-{step}");
}
}
else
{
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out _))
{
sprite.LayerSetVisible(GunVisualLayers.Mag, false);
}
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out _))
{
sprite.LayerSetVisible(GunVisualLayers.MagUnshaded, false);
}
}
}
}
public enum GunVisualLayers : byte
{
Base,
BaseUnshaded,
Mag,
MagUnshaded,
}

View File

@@ -0,0 +1,20 @@
using Content.Client.Weapons.Ranged.Systems;
namespace Content.Client.Weapons.Ranged.Components;
[RegisterComponent, Friend(typeof(GunSystem))]
public sealed class SpentAmmoVisualsComponent : Component
{
/// <summary>
/// Should we do "{_state}-spent" or just "spent"
/// </summary>
[DataField("suffix")] public bool Suffix = true;
[DataField("state")]
public string State = "base";
}
public enum AmmoVisualLayers : byte
{
Base,
}

View File

@@ -1,31 +0,0 @@
using Content.Client.Weapons.Ranged.Barrels.Components;
using Content.Shared.Weapons.Ranged;
using Robust.Client.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Client.Weapons.Ranged;
public sealed class GunSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<MagazineAutoEjectEvent>(OnMagAutoEject);
}
private void OnMagAutoEject(MagazineAutoEjectEvent ev)
{
var player = _playerManager.LocalPlayer?.ControlledEntity;
if (!TryComp(ev.Uid, out ClientMagazineBarrelComponent? mag) ||
!_container.TryGetContainingContainer(ev.Uid, out var container) ||
container.Owner != player) return;
mag.PlayAlarmAnimation();
}
}

View File

@@ -1,110 +0,0 @@
using System;
using Content.Client.CombatMode;
using Content.Shared.Hands.Components;
using Content.Shared.Weapons.Ranged.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Client.Weapons.Ranged
{
[UsedImplicitly]
public sealed class RangedWeaponSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly CombatModeSystem _combatModeSystem = default!;
private bool _blocked;
private int _shotCounter;
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_gameTiming.IsFirstTimePredicted)
{
return;
}
var state = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use);
if (!_combatModeSystem.IsInCombatMode() || state != BoundKeyState.Down)
{
_shotCounter = 0;
_blocked = false;
return;
}
var entity = _playerManager.LocalPlayer?.ControlledEntity;
if (!EntityManager.TryGetComponent(entity, out SharedHandsComponent? hands))
{
return;
}
if (hands.ActiveHandEntity is not EntityUid held || !EntityManager.TryGetComponent(held, out ClientRangedWeaponComponent? weapon))
{
_blocked = true;
return;
}
switch (weapon.FireRateSelector)
{
case FireRateSelector.Safety:
_blocked = true;
return;
case FireRateSelector.Single:
if (_shotCounter >= 1)
{
_blocked = true;
return;
}
break;
case FireRateSelector.Automatic:
break;
default:
throw new ArgumentOutOfRangeException();
}
if (_blocked)
return;
var mapCoordinates = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
EntityCoordinates coordinates;
if (_mapManager.TryFindGridAt(mapCoordinates, out var grid))
{
coordinates = EntityCoordinates.FromMap(grid.GridEntityId, mapCoordinates);
}
else
{
coordinates = EntityCoordinates.FromMap(_mapManager.GetMapEntityId(mapCoordinates.MapId), mapCoordinates);
}
SyncFirePos(coordinates);
}
private void SyncFirePos(EntityCoordinates coordinates)
{
RaiseNetworkEvent(new FirePosEvent(coordinates));
}
}
}

View File

@@ -1,12 +1,13 @@
using Content.Client.Projectiles;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Client.Weapons.Ranged;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
{

View File

@@ -0,0 +1,513 @@
using Content.Client.IoC;
using Content.Client.Items;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Client.Weapons.Ranged.Components;
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(EntityUid uid, AmmoCounterComponent component, ItemStatusCollectMessage args)
{
RefreshControl(uid, component);
if (component.Control != null)
args.Controls.Add(component.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(EntityUid uid, AmmoCounterComponent? component = null)
{
if (!Resolve(uid, ref component, false)) return;
component.Control?.Dispose();
component.Control = null;
var ev = new AmmoCounterControlEvent();
RaiseLocalEvent(uid, ev, false);
// Fallback to default if none specified
ev.Control ??= new DefaultStatusControl();
component.Control = ev.Control;
UpdateAmmoCount(uid, component);
}
private void UpdateAmmoCount(EntityUid uid, AmmoCounterComponent component)
{
if (component.Control == null) return;
var ev = new UpdateAmmoCounterEvent()
{
Control = component.Control
};
RaiseLocalEvent(uid, ev, false);
}
protected override void UpdateAmmoCount(EntityUid uid)
{
// 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 (!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 BoxContainer _bulletsListTop;
private readonly BoxContainer _bulletsListBottom;
public DefaultStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0,
Children =
{
(_bulletsListTop = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
SeparationOverride = 0
}),
new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsListBottom = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}),
}
},
}
}
}
});
}
public void Update(int count, int capacity)
{
_bulletsListTop.RemoveAllChildren();
_bulletsListBottom.RemoveAllChildren();
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);
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;
}
}
}
public sealed class BoxesStatusControl : Control
{
private readonly BoxContainer _bulletsList;
private readonly Label _ammoCount;
public BoxesStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsList = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 4
}),
}
},
new Control() { MinSize = (5, 0) },
(_ammoCount = new Label
{
StyleClasses = { StyleNano.StyleClassItemStatus },
HorizontalAlignment = HAlignment.Right,
}),
}
});
}
public void Update(int count, int max)
{
_bulletsList.RemoveAllChildren();
_ammoCount.Visible = true;
_ammoCount.Text = $"x{count:00}";
max = Math.Min(max, 8);
FillBulletRow(_bulletsList, count, max);
}
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,
},
MinSize = (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,
},
MinSize = (10, 15),
});
}
}
}
private sealed class ChamberMagazineStatusControl : Control
{
private readonly BoxContainer _bulletsList;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
private readonly Label _ammoCount;
public ChamberMagazineStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
VerticalAlignment = VAlignment.Center;
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children =
{
(_chamberedBullet = new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered_rotated.png"),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Right,
}),
new Control() { MinSize = (5,0) },
new Control
{
HorizontalExpand = true,
Children =
{
(_bulletsList = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 0
}),
(_noMagazineLabel = new Label
{
Text = "No Magazine!",
StyleClasses = {StyleNano.StyleClassItemStatus}
})
}
},
new Control() { MinSize = (5,0) },
(_ammoCount = new Label
{
StyleClasses = {StyleNano.StyleClassItemStatus},
HorizontalAlignment = HAlignment.Right,
}),
}
});
}
public void Update(bool chambered, bool magazine, int count, int capacity)
{
_chamberedBullet.ModulateSelfOverride =
chambered ? Color.FromHex("#d7df60") : Color.Black;
_bulletsList.RemoveAllChildren();
if (!magazine)
{
_noMagazineLabel.Visible = true;
_ammoCount.Visible = false;
return;
}
_noMagazineLabel.Visible = false;
_ammoCount.Visible = true;
var texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
var texture = StaticIoC.ResC.GetTexture(texturePath);
_ammoCount.Text = $"x{count:00}";
capacity = Math.Min(capacity, 20);
FillBulletRow(_bulletsList, 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;
// Draw the empty ones
for (var i = count; i < capacity; i++)
{
container.AddChild(new TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorGoneA : colorGoneB,
Stretch = TextureRect.StretchMode.KeepCentered
});
altColor ^= true;
}
// 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 TextureRect
{
Texture = texture,
ModulateSelfOverride = altColor ? colorA : colorB,
Stretch = TextureRect.StretchMode.KeepCentered
});
altColor ^= true;
}
}
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 = 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 = (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
}

View File

@@ -0,0 +1,48 @@
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Map;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeBallistic()
{
base.InitializeBallistic();
SubscribeLocalEvent<BallisticAmmoProviderComponent, UpdateAmmoCounterEvent>(OnBallisticAmmoCount);
}
private void OnBallisticAmmoCount(EntityUid uid, BallisticAmmoProviderComponent component, UpdateAmmoCounterEvent args)
{
if (args.Control is DefaultStatusControl control)
{
control.Update(GetBallisticShots(component), component.Capacity);
return;
}
}
protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates)
{
if (!Timing.IsFirstTimePredicted) return;
EntityUid? ent = null;
// TODO: Combine with TakeAmmo
if (component.Entities.Count > 0)
{
var existing = component.Entities[^1];
component.Entities.RemoveAt(component.Entities.Count - 1);
component.Container.Remove(existing);
EnsureComp<AmmoComponent>(existing);
}
else if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
ent = Spawn(component.FillProto, coordinates);
EnsureComp<AmmoComponent>(ent.Value);
}
if (ent != null && ent.Value.IsClientSide())
Del(ent.Value);
}
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
}
private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
{
if (args.Control is not BoxesStatusControl boxes) return;
boxes.Update(component.Shots, component.Capacity);
}
private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
{
args.Control = new BoxesStatusControl();
}
}

View File

@@ -0,0 +1,50 @@
using Content.Shared.Examine;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Containers;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeChamberMagazine()
{
base.InitializeChamberMagazine();
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, AmmoCounterControlEvent>(OnChamberMagazineCounter);
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, UpdateAmmoCounterEvent>(OnChamberMagazineAmmoUpdate);
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, EntRemovedFromContainerMessage>(OnChamberEntRemove);
}
private void OnChamberEntRemove(EntityUid uid, ChamberMagazineAmmoProviderComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != ChamberSlot) return;
// This is dirty af. Prediction moment.
// We may be predicting spawning entities and the engine just removes them from the container so we'll just delete them.
if (args.Entity.IsClientSide())
QueueDel(args.Entity);
// AFAIK the only main alternative is having some client-specific handling via a bool or otherwise for the state.
// which is much larger and I'm not sure how much better it is. It's bad enough we have to do it with revolvers
// to avoid 6-7 additional entity spawns.
}
private void OnChamberMagazineCounter(EntityUid uid, ChamberMagazineAmmoProviderComponent component, AmmoCounterControlEvent args)
{
args.Control = new ChamberMagazineStatusControl();
}
private void OnChamberMagazineAmmoUpdate(EntityUid uid, ChamberMagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
{
if (args.Control is not ChamberMagazineStatusControl control) return;
var chambered = GetChamberEntity(uid);
var magEntity = GetMagazineEntity(uid);
var ammoCountEv = new GetAmmoCountEvent();
if (magEntity != null)
RaiseLocalEvent(magEntity.Value, ref ammoCountEv, false);
control.Update(chambered != null, magEntity != null, ammoCountEv.Count, ammoCountEv.Capacity);
}
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.Weapons.Ranged;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeMagazine()
{
base.InitializeMagazine();
SubscribeLocalEvent<MagazineAmmoProviderComponent, UpdateAmmoCounterEvent>(OnMagazineAmmoUpdate);
}
private void OnMagazineAmmoUpdate(EntityUid uid, MagazineAmmoProviderComponent component, UpdateAmmoCounterEvent args)
{
var ent = GetMagazineEntity(uid);
if (ent == null)
{
if (args.Control is DefaultStatusControl control)
{
control.Update(0, 0);
}
return;
}
RaiseLocalEvent(ent.Value, args, false);
}
}

View File

@@ -0,0 +1,37 @@
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Containers;
using Robust.Shared.Utility;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeRevolver()
{
base.InitializeRevolver();
SubscribeLocalEvent<RevolverAmmoProviderComponent, AmmoCounterControlEvent>(OnRevolverCounter);
SubscribeLocalEvent<RevolverAmmoProviderComponent, UpdateAmmoCounterEvent>(OnRevolverAmmoUpdate);
SubscribeLocalEvent<RevolverAmmoProviderComponent, EntRemovedFromContainerMessage>(OnRevolverEntRemove);
}
private void OnRevolverEntRemove(EntityUid uid, RevolverAmmoProviderComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != RevolverContainer) return;
// See ChamberMagazineAmmoProvider
if (!args.Entity.IsClientSide()) return;
QueueDel(args.Entity);
}
private void OnRevolverAmmoUpdate(EntityUid uid, RevolverAmmoProviderComponent component, UpdateAmmoCounterEvent args)
{
if (args.Control is not RevolverStatusControl control) return;
control.Update(component.CurrentIndex, component.Chambers);
}
private void OnRevolverCounter(EntityUid uid, RevolverAmmoProviderComponent component, AmmoCounterControlEvent args)
{
args.Control = new RevolverStatusControl();
}
}

View File

@@ -0,0 +1,34 @@
using Content.Client.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
private void InitializeSpentAmmo()
{
SubscribeLocalEvent<SpentAmmoVisualsComponent, AppearanceChangeEvent>(OnSpentAmmoAppearance);
}
private void OnSpentAmmoAppearance(EntityUid uid, SpentAmmoVisualsComponent component, ref AppearanceChangeEvent args)
{
var sprite = args.Sprite;
if (sprite == null) return;
if (!args.AppearanceData.TryGetValue(AmmoVisuals.Spent, out var varSpent))
{
return;
}
var spent = (bool) varSpent;
string state;
if (spent)
state = component.Suffix ? $"{component.State}-spent" : "spent";
else
state = component.State;
sprite.LayerSetState(AmmoVisualLayers.Base, state);
}
}

View File

@@ -0,0 +1,187 @@
using Content.Client.Items;
using Content.Client.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly AnimationPlayerSystem _animPlayer = default!;
[Dependency] private readonly EffectSystem _effects = default!;
[Dependency] private readonly InputSystem _inputSystem = default!;
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
// Plays animated effects on the client.
SubscribeNetworkEvent<HitscanEvent>(OnHitscan);
InitializeSpentAmmo();
}
private void OnHitscan(HitscanEvent ev)
{
// ALL I WANT IS AN ANIMATED EFFECT
foreach (var a in ev.Sprites)
{
if (a.Sprite is not SpriteSpecifier.Rsi rsi) continue;
var ent = Spawn("HitscanEffect", a.coordinates);
var sprite = Comp<SpriteComponent>(ent);
var xform = Transform(ent);
xform.LocalRotation = a.angle;
sprite[EffectLayers.Unshaded].AutoAnimated = false;
sprite.LayerSetSprite(EffectLayers.Unshaded, rsi);
sprite.LayerSetState(EffectLayers.Unshaded, rsi.RsiState);
sprite.Scale = new Vector2(a.Distance, 1f);
sprite[EffectLayers.Unshaded].Visible = true;
var anim = new Animation()
{
Length = TimeSpan.FromSeconds(0.48f),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
{
LayerKey = EffectLayers.Unshaded,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(rsi.RsiState, 0f),
}
}
}
};
_animPlayer.Play(ent, null, anim, "hitscan-effect");
}
}
public override void Update(float frameTime)
{
if (!Timing.IsFirstTimePredicted)
return;
var entityNull = _player.LocalPlayer?.ControlledEntity;
if (entityNull == null)
{
return;
}
var entity = entityNull.Value;
var gun = GetGun(entity);
if (gun == null)
{
return;
}
if (_inputSystem.CmdStates.GetState(EngineKeyFunctions.Use) != BoundKeyState.Down)
{
if (gun.ShotCounter != 0)
EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = gun.Owner });
return;
}
if (gun.NextFire > Timing.CurTime)
return;
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
EntityCoordinates coordinates;
// Bro why would I want a ternary here
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (MapManager.TryFindGridAt(mousePos, out var grid))
{
coordinates = EntityCoordinates.FromMap(grid.GridEntityId, mousePos, EntityManager);
}
else
{
coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, EntityManager);
}
Sawmill.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
EntityManager.RaisePredictiveEvent(new RequestShootEvent
{
Coordinates = coordinates,
Gun = gun.Owner,
});
}
public override void Shoot(GunComponent gun, List<IShootable> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null)
{
// Rather than splitting client / server for every ammo provider it's easier
// to just delete the spawned entities. This is for programmer sanity despite the wasted perf.
// This also means any ammo specific stuff can be grabbed as necessary.
foreach (var ent in ammo)
{
switch (ent)
{
case CartridgeAmmoComponent cartridge:
if (!cartridge.Spent)
{
SetCartridgeSpent(cartridge, true);
MuzzleFlash(gun.Owner, cartridge, user);
// TODO: Can't predict entity deletions.
//if (cartridge.DeleteOnSpawn)
// Del(cartridge.Owner);
}
else
{
PlaySound(gun.Owner, gun.SoundEmpty?.GetSound(Random, ProtoManager), user);
}
if (cartridge.Owner.IsClientSide())
Del(cartridge.Owner);
break;
case AmmoComponent newAmmo:
MuzzleFlash(gun.Owner, newAmmo, user);
if (newAmmo.Owner.IsClientSide())
Del(newAmmo.Owner);
else
RemComp<AmmoComponent>(newAmmo.Owner);
break;
}
}
}
protected override void PlaySound(EntityUid gun, string? sound, EntityUid? user = null)
{
if (sound == null || user == null || !Timing.IsFirstTimePredicted) return;
SoundSystem.Play(Filter.Local(), sound, gun);
}
protected override void Popup(string message, EntityUid? uid, EntityUid? user)
{
if (uid == null || user == null || !Timing.IsFirstTimePredicted) return;
PopupSystem.PopupEntity(message, uid.Value, Filter.Entities(user.Value));
}
protected override void CreateEffect(EffectSystemMessage message, EntityUid? user = null)
{
_effects.CreateEffect(message);
}
}

View File

@@ -1,5 +1,5 @@
using Content.Client.Clickable;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
@@ -7,7 +7,7 @@ using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Client.Weapons.Ranged;
namespace Content.Client.Weapons.Ranged.Systems;
public sealed class TetherGunSystem : SharedTetherGunSystem
{