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:
@@ -23,7 +23,6 @@
|
|||||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
|
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
|
||||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Analyzers.targets" />
|
<Import Project="..\RobustToolbox\MSBuild\Robust.Analyzers.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
17
Content.Client/Effects/EffectVisualizerSystem.cs
Normal file
17
Content.Client/Effects/EffectVisualizerSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Content.Client/Effects/EffectVisualsComponent.cs
Normal file
8
Content.Client/Effects/EffectVisualsComponent.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Content.Client.Effects;
|
||||||
|
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class EffectVisualsComponent : Component
|
||||||
|
{
|
||||||
|
public float Length;
|
||||||
|
public float Accumulator = 0f;
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Robust.Client.UserInterface;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Robust.Client.UserInterface;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
|
|
||||||
namespace Content.Client.Items
|
namespace Content.Client.Items
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Client.Weapons.Ranged.Systems;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
|
|
||||||
namespace Content.Client.Weapons.Ranged;
|
namespace Content.Client.Weapons.Ranged;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
107
Content.Client/Weapons/Ranged/Components/MagVisualizer.cs
Normal file
107
Content.Client/Weapons/Ranged/Components/MagVisualizer.cs
Normal 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,
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
using Content.Client.Projectiles;
|
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.Client.Player;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Physics.Dynamics;
|
using Robust.Shared.Physics.Dynamics;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Client.Weapons.Ranged;
|
namespace Content.Client.Weapons.Ranged.Systems;
|
||||||
|
|
||||||
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
|
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
|
||||||
{
|
{
|
||||||
513
Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
Normal file
513
Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
Normal 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
|
||||||
|
}
|
||||||
48
Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs
Normal file
48
Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs
Normal file
30
Content.Client/Weapons/Ranged/Systems/GunSystem.Battery.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs
Normal file
29
Content.Client/Weapons/Ranged/Systems/GunSystem.Magazine.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs
Normal file
37
Content.Client/Weapons/Ranged/Systems/GunSystem.Revolver.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs
Normal file
34
Content.Client/Weapons/Ranged/Systems/GunSystem.SpentAmmo.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
187
Content.Client/Weapons/Ranged/Systems/GunSystem.cs
Normal file
187
Content.Client/Weapons/Ranged/Systems/GunSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using Content.Client.Clickable;
|
using Content.Client.Clickable;
|
||||||
using Content.Shared.Weapons.Ranged;
|
using Content.Shared.Weapons.Ranged.Systems;
|
||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.Input;
|
using Robust.Client.Input;
|
||||||
@@ -7,7 +7,7 @@ using Robust.Shared.Input;
|
|||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Client.Weapons.Ranged;
|
namespace Content.Client.Weapons.Ranged.Systems;
|
||||||
|
|
||||||
public sealed class TetherGunSystem : SharedTetherGunSystem
|
public sealed class TetherGunSystem : SharedTetherGunSystem
|
||||||
{
|
{
|
||||||
@@ -9,6 +9,7 @@ namespace Content.Server.Entry
|
|||||||
"StasisBedVisuals",
|
"StasisBedVisuals",
|
||||||
"InteractionOutline",
|
"InteractionOutline",
|
||||||
"MeleeWeaponArcAnimation",
|
"MeleeWeaponArcAnimation",
|
||||||
|
"EffectVisuals",
|
||||||
"AnimationsTest",
|
"AnimationsTest",
|
||||||
"ItemStatus",
|
"ItemStatus",
|
||||||
"VehicleVisuals",
|
"VehicleVisuals",
|
||||||
@@ -21,11 +22,12 @@ namespace Content.Server.Entry
|
|||||||
"LatheVisuals",
|
"LatheVisuals",
|
||||||
"DiseaseMachineVisuals",
|
"DiseaseMachineVisuals",
|
||||||
"HandheldGPS",
|
"HandheldGPS",
|
||||||
|
"SpentAmmoVisuals",
|
||||||
"ToggleableLightVisuals",
|
"ToggleableLightVisuals",
|
||||||
"CableVisualizer",
|
"CableVisualizer",
|
||||||
"PotencyVisuals",
|
"PotencyVisuals",
|
||||||
"PaperVisuals",
|
"PaperVisuals",
|
||||||
"SurveillanceCameraVisuals"
|
"SurveillanceCameraVisuals",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Content.Server.CombatMode;
|
|||||||
using Content.Server.Hands.Components;
|
using Content.Server.Hands.Components;
|
||||||
using Content.Server.Pulling;
|
using Content.Server.Pulling;
|
||||||
using Content.Server.Storage.Components;
|
using Content.Server.Storage.Components;
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.ActionBlocker;
|
using Content.Shared.ActionBlocker;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.DragDrop;
|
using Content.Shared.DragDrop;
|
||||||
@@ -13,7 +12,6 @@ using Content.Shared.Interaction.Events;
|
|||||||
using Content.Shared.Item;
|
using Content.Shared.Item;
|
||||||
using Content.Shared.Pulling.Components;
|
using Content.Shared.Pulling.Components;
|
||||||
using Content.Shared.Weapons.Melee;
|
using Content.Shared.Weapons.Melee;
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
|
|||||||
@@ -15,29 +15,26 @@ namespace Content.Server.Power.Components
|
|||||||
private CellChargerStatus _status;
|
private CellChargerStatus _status;
|
||||||
|
|
||||||
[DataField("chargeRate")]
|
[DataField("chargeRate")]
|
||||||
private int _chargeRate = 100;
|
public int ChargeRate = 20;
|
||||||
|
|
||||||
[DataField("transferEfficiency")]
|
|
||||||
private float _transferEfficiency = 0.85f;
|
|
||||||
|
|
||||||
[DataField("chargerSlot", required: true)]
|
[DataField("chargerSlot", required: true)]
|
||||||
public ItemSlot ChargerSlot = new();
|
public ItemSlot ChargerSlot = new();
|
||||||
|
|
||||||
private CellChargerStatus GetStatus()
|
private CellChargerStatus GetStatus()
|
||||||
{
|
{
|
||||||
if (_entMan.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) &&
|
if (!_entMan.TryGetComponent<TransformComponent>(Owner, out var xform) ||
|
||||||
!receiver.Powered)
|
!xform.Anchored ||
|
||||||
|
_entMan.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) && !receiver.Powered)
|
||||||
{
|
{
|
||||||
return CellChargerStatus.Off;
|
return CellChargerStatus.Off;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ChargerSlot.HasItem)
|
if (!ChargerSlot.HasItem)
|
||||||
{
|
|
||||||
return CellChargerStatus.Empty;
|
return CellChargerStatus.Empty;
|
||||||
}
|
|
||||||
if (HeldBattery != null && Math.Abs(HeldBattery.MaxCharge - HeldBattery.CurrentCharge) < 0.01)
|
if (HeldBattery != null && Math.Abs(HeldBattery.MaxCharge - HeldBattery.CurrentCharge) < 0.01)
|
||||||
{
|
|
||||||
return CellChargerStatus.Charged;
|
return CellChargerStatus.Charged;
|
||||||
}
|
|
||||||
return CellChargerStatus.Charging;
|
return CellChargerStatus.Charging;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +63,7 @@ namespace Content.Server.Power.Components
|
|||||||
appearance?.SetData(CellVisual.Light, CellChargerStatus.Empty);
|
appearance?.SetData(CellVisual.Light, CellChargerStatus.Empty);
|
||||||
break;
|
break;
|
||||||
case CellChargerStatus.Charging:
|
case CellChargerStatus.Charging:
|
||||||
receiver.Load = (int) (_chargeRate / _transferEfficiency);
|
receiver.Load = ChargeRate;
|
||||||
appearance?.SetData(CellVisual.Light, CellChargerStatus.Charging);
|
appearance?.SetData(CellVisual.Light, CellChargerStatus.Charging);
|
||||||
break;
|
break;
|
||||||
case CellChargerStatus.Charged:
|
case CellChargerStatus.Charged:
|
||||||
@@ -83,9 +80,8 @@ namespace Content.Server.Power.Components
|
|||||||
public void OnUpdate(float frameTime) //todo: make single system for this
|
public void OnUpdate(float frameTime) //todo: make single system for this
|
||||||
{
|
{
|
||||||
if (_status == CellChargerStatus.Empty || _status == CellChargerStatus.Charged || !ChargerSlot.HasItem)
|
if (_status == CellChargerStatus.Empty || _status == CellChargerStatus.Charged || !ChargerSlot.HasItem)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
TransferPower(frameTime);
|
TransferPower(frameTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,16 +94,15 @@ namespace Content.Server.Power.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (HeldBattery == null)
|
if (HeldBattery == null)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
HeldBattery.CurrentCharge += _chargeRate * frameTime;
|
HeldBattery.CurrentCharge += ChargeRate * frameTime;
|
||||||
// Just so the sprite won't be set to 99.99999% visibility
|
// Just so the sprite won't be set to 99.99999% visibility
|
||||||
if (HeldBattery.MaxCharge - HeldBattery.CurrentCharge < 0.01)
|
if (HeldBattery.MaxCharge - HeldBattery.CurrentCharge < 0.01)
|
||||||
{
|
{
|
||||||
HeldBattery.CurrentCharge = HeldBattery.MaxCharge;
|
HeldBattery.CurrentCharge = HeldBattery.MaxCharge;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateStatus();
|
UpdateStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Content.Server.Power.Components;
|
using Content.Server.Power.Components;
|
||||||
using Content.Server.PowerCell;
|
using Content.Server.PowerCell;
|
||||||
using Content.Shared.Containers.ItemSlots;
|
using Content.Shared.Containers.ItemSlots;
|
||||||
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.PowerCell.Components;
|
using Content.Shared.PowerCell.Components;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
@@ -15,16 +16,18 @@ internal sealed class ChargerSystem : EntitySystem
|
|||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
SubscribeLocalEvent<ChargerComponent, ComponentInit>(OnChargerInit);
|
SubscribeLocalEvent<ChargerComponent, ComponentInit>(OnChargerInit);
|
||||||
SubscribeLocalEvent<ChargerComponent, ComponentRemove>(OnChargerRemove);
|
SubscribeLocalEvent<ChargerComponent, ComponentRemove>(OnChargerRemove);
|
||||||
|
|
||||||
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
|
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
|
||||||
|
|
||||||
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
|
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
|
||||||
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
|
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
|
||||||
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
|
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
|
||||||
|
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", component.ChargeRate)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged;
|
|
||||||
using Content.Shared.Damage;
|
|
||||||
using Content.Shared.Physics;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Timing;
|
|
||||||
|
|
||||||
namespace Content.Server.Projectiles.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Lasers etc.
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class HitscanComponent : Component
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
|
|
||||||
public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask;
|
|
||||||
|
|
||||||
[DataField("layers")] //todo WithFormat.Flags<CollisionLayer>()
|
|
||||||
private int _collisionMask = (int) CollisionGroup.Opaque;
|
|
||||||
|
|
||||||
[DataField("damage", required: true)]
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public DamageSpecifier Damage = default!;
|
|
||||||
|
|
||||||
public float MaxLength => 20.0f;
|
|
||||||
private TimeSpan _startTime;
|
|
||||||
private TimeSpan _deathTime;
|
|
||||||
|
|
||||||
public float ColorModifier { get; set; } = 1.0f;
|
|
||||||
[DataField("spriteName")]
|
|
||||||
private string _spriteName = "Objects/Weapons/Guns/Projectiles/laser.png";
|
|
||||||
[DataField("muzzleFlash")]
|
|
||||||
private string? _muzzleFlash;
|
|
||||||
[DataField("impactFlash")]
|
|
||||||
private string? _impactFlash;
|
|
||||||
|
|
||||||
[DataField("soundHit")]
|
|
||||||
public SoundSpecifier? SoundHit;
|
|
||||||
|
|
||||||
[DataField("soundForce")]
|
|
||||||
public bool ForceSound = false;
|
|
||||||
|
|
||||||
public void FireEffects(EntityUid user, float distance, Angle angle, EntityUid? hitEntity = null)
|
|
||||||
{
|
|
||||||
var effectSystem = EntitySystem.Get<EffectSystem>();
|
|
||||||
_startTime = _gameTiming.CurTime;
|
|
||||||
_deathTime = _startTime + TimeSpan.FromSeconds(1);
|
|
||||||
|
|
||||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
|
||||||
|
|
||||||
// We'll get the effects relative to the grid / map of the firer
|
|
||||||
var gridOrMap = _entMan.GetComponent<TransformComponent>(user).GridID == GridId.Invalid ? mapManager.GetMapEntityId(_entMan.GetComponent<TransformComponent>(user).MapID) :
|
|
||||||
mapManager.GetGrid(_entMan.GetComponent<TransformComponent>(user).GridID).GridEntityId;
|
|
||||||
|
|
||||||
var parentXform = _entMan.GetComponent<TransformComponent>(gridOrMap);
|
|
||||||
|
|
||||||
var localCoordinates = new EntityCoordinates(gridOrMap, parentXform.InvWorldMatrix.Transform(_entMan.GetComponent<TransformComponent>(user).WorldPosition));
|
|
||||||
var localAngle = angle - parentXform.WorldRotation;
|
|
||||||
|
|
||||||
var afterEffect = AfterEffects(localCoordinates, localAngle, distance, 1.0f);
|
|
||||||
if (afterEffect != null)
|
|
||||||
{
|
|
||||||
effectSystem.CreateParticle(afterEffect);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we're too close we'll stop the impact and muzzle / impact sprites from clipping
|
|
||||||
if (distance > 1.0f)
|
|
||||||
{
|
|
||||||
var impactEffect = ImpactFlash(distance, localAngle);
|
|
||||||
if (impactEffect != null)
|
|
||||||
{
|
|
||||||
effectSystem.CreateParticle(impactEffect);
|
|
||||||
}
|
|
||||||
|
|
||||||
var muzzleEffect = MuzzleFlash(localCoordinates, localAngle);
|
|
||||||
if (muzzleEffect != null)
|
|
||||||
{
|
|
||||||
effectSystem.CreateParticle(muzzleEffect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Owner.SpawnTimer((int) _deathTime.TotalMilliseconds, () =>
|
|
||||||
{
|
|
||||||
if (!_entMan.Deleted(Owner))
|
|
||||||
{
|
|
||||||
_entMan.DeleteEntity(Owner);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private EffectSystemMessage? MuzzleFlash(EntityCoordinates grid, Angle angle)
|
|
||||||
{
|
|
||||||
if (_muzzleFlash == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var offset = angle.ToVec().Normalized / 2;
|
|
||||||
|
|
||||||
var message = new EffectSystemMessage
|
|
||||||
{
|
|
||||||
EffectSprite = _muzzleFlash,
|
|
||||||
Born = _startTime,
|
|
||||||
DeathTime = _deathTime,
|
|
||||||
Coordinates = grid.Offset(offset),
|
|
||||||
//Rotated from east facing
|
|
||||||
Rotation = (float) angle.Theta,
|
|
||||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
|
|
||||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
|
||||||
Shaded = false
|
|
||||||
};
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EffectSystemMessage AfterEffects(EntityCoordinates origin, Angle angle, float distance, float offset = 0.0f)
|
|
||||||
{
|
|
||||||
var midPointOffset = angle.ToVec() * distance / 2;
|
|
||||||
var message = new EffectSystemMessage
|
|
||||||
{
|
|
||||||
EffectSprite = _spriteName,
|
|
||||||
Born = _startTime,
|
|
||||||
DeathTime = _deathTime,
|
|
||||||
Size = new Vector2(distance - offset, 1f),
|
|
||||||
Coordinates = origin.Offset(midPointOffset),
|
|
||||||
//Rotated from east facing
|
|
||||||
Rotation = (float) angle.Theta,
|
|
||||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
|
|
||||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
|
||||||
|
|
||||||
Shaded = false
|
|
||||||
};
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EffectSystemMessage? ImpactFlash(float distance, Angle angle)
|
|
||||||
{
|
|
||||||
if (_impactFlash == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = new EffectSystemMessage
|
|
||||||
{
|
|
||||||
EffectSprite = _impactFlash,
|
|
||||||
Born = _startTime,
|
|
||||||
DeathTime = _deathTime,
|
|
||||||
Coordinates = _entMan.GetComponent<TransformComponent>(Owner).Coordinates.Offset(angle.ToVec() * distance),
|
|
||||||
//Rotated from east facing
|
|
||||||
Rotation = (float) angle.FlipPositive(),
|
|
||||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
|
|
||||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
|
||||||
Shaded = false
|
|
||||||
};
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ using Robust.Shared.Audio;
|
|||||||
using Robust.Shared.Physics.Dynamics;
|
using Robust.Shared.Physics.Dynamics;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
|
using GunSystem = Content.Server.Weapon.Ranged.Systems.GunSystem;
|
||||||
|
|
||||||
namespace Content.Server.Projectiles
|
namespace Content.Server.Projectiles
|
||||||
{
|
{
|
||||||
@@ -51,7 +52,7 @@ namespace Content.Server.Projectiles
|
|||||||
$"Projectile {ToPrettyString(component.Owner):projectile} shot by {ToPrettyString(component.Shooter):user} hit {ToPrettyString(otherEntity):target} and dealt {modifiedDamage.Total:damage} damage");
|
$"Projectile {ToPrettyString(component.Owner):projectile} shot by {ToPrettyString(component.Shooter):user} hit {ToPrettyString(otherEntity):target} and dealt {modifiedDamage.Total:damage} damage");
|
||||||
}
|
}
|
||||||
|
|
||||||
_guns.PlaySound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
|
_guns.PlayImpactSound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
|
||||||
|
|
||||||
// Damaging it can delete it
|
// Damaging it can delete it
|
||||||
if (HasComp<CameraRecoilComponent>(otherEntity))
|
if (HasComp<CameraRecoilComponent>(otherEntity))
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ namespace Content.Server.Tools
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Sprite is optional.
|
// Sprite is optional.
|
||||||
Resolve(uid, ref sprite);
|
Resolve(uid, ref sprite, false);
|
||||||
|
|
||||||
if (multiple.Entries.Length == 0)
|
if (multiple.Entries.Length == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Stores ammo and can quickly transfer ammo into a magazine.
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
[Friend(typeof(GunSystem))]
|
|
||||||
public sealed class AmmoBoxComponent : Component
|
|
||||||
{
|
|
||||||
[DataField("caliber")]
|
|
||||||
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
[DataField("capacity")]
|
|
||||||
public int Capacity
|
|
||||||
{
|
|
||||||
get => _capacity;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_capacity = value;
|
|
||||||
SpawnedAmmo = new Stack<EntityUid>(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int _capacity = 30;
|
|
||||||
|
|
||||||
public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount;
|
|
||||||
public Stack<EntityUid> SpawnedAmmo = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Container that holds any instantiated ammo.
|
|
||||||
/// </summary>
|
|
||||||
public Container AmmoContainer = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many more deferred entities can be spawned. We defer these to avoid instantiating the entities until needed for performance reasons.
|
|
||||||
/// </summary>
|
|
||||||
public int UnspawnedCount;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The prototype of the ammo to be retrieved when getting ammo.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? FillPrototype;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Allows this entity to be loaded into a ranged weapon (if the caliber matches)
|
|
||||||
/// Generally used for bullets but can be used for other things like bananas
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
[Friend(typeof(GunSystem))]
|
|
||||||
public sealed class AmmoComponent : Component, ISerializationHooks
|
|
||||||
{
|
|
||||||
[DataField("caliber")]
|
|
||||||
public BallisticCaliber Caliber { get; } = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
public bool Spent
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (AmmoIsProjectile)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _spent;
|
|
||||||
}
|
|
||||||
set => _spent = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _spent;
|
|
||||||
|
|
||||||
// TODO: Make it so null projectile = dis
|
|
||||||
/// <summary>
|
|
||||||
/// Used for anything without a case that fires itself
|
|
||||||
/// </summary>
|
|
||||||
[DataField("isProjectile")] public bool AmmoIsProjectile;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used for something that is deleted when the projectile is retrieved
|
|
||||||
/// </summary>
|
|
||||||
[DataField("caseless")]
|
|
||||||
public bool Caseless { get; }
|
|
||||||
|
|
||||||
// Rather than managing bullet / case state seemed easier to just have 2 toggles
|
|
||||||
// ammoIsProjectile being for a beanbag for example and caseless being for ClRifle rounds
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// For shotguns where they might shoot multiple entities
|
|
||||||
/// </summary>
|
|
||||||
[DataField("projectilesFired")]
|
|
||||||
public int ProjectilesFired { get; } = 1;
|
|
||||||
|
|
||||||
[DataField("projectile", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? ProjectileId;
|
|
||||||
|
|
||||||
// How far apart each entity is if multiple are shot
|
|
||||||
[DataField("ammoSpread")]
|
|
||||||
public float EvenSpreadAngle { get; } = default;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How fast the shot entities travel
|
|
||||||
/// </summary>
|
|
||||||
[DataField("ammoVelocity")]
|
|
||||||
public float Velocity { get; } = 20f;
|
|
||||||
|
|
||||||
[DataField("muzzleFlash")]
|
|
||||||
public ResourcePath? MuzzleFlashSprite = new("Objects/Weapons/Guns/Projectiles/bullet_muzzle.png");
|
|
||||||
|
|
||||||
[DataField("soundCollectionEject")]
|
|
||||||
public SoundSpecifier SoundCollectionEject { get; } = new SoundCollectionSpecifier("CasingEject");
|
|
||||||
|
|
||||||
void ISerializationHooks.AfterDeserialization()
|
|
||||||
{
|
|
||||||
// Being both caseless and shooting yourself doesn't make sense
|
|
||||||
DebugTools.Assert(!(AmmoIsProjectile && Caseless));
|
|
||||||
|
|
||||||
if (ProjectilesFired < 1)
|
|
||||||
{
|
|
||||||
Logger.Error("Ammo can't have less than 1 projectile");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EvenSpreadAngle > 0 && ProjectilesFired == 1)
|
|
||||||
{
|
|
||||||
Logger.Error("Can't have an even spread if only 1 projectile is fired");
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum BallisticCaliber
|
|
||||||
{
|
|
||||||
Unspecified = 0,
|
|
||||||
A357, // Placeholder?
|
|
||||||
ClRifle,
|
|
||||||
SRifle,
|
|
||||||
Pistol,
|
|
||||||
A35, // Placeholder?
|
|
||||||
LRifle,
|
|
||||||
HRifle,
|
|
||||||
Magnum,
|
|
||||||
AntiMaterial,
|
|
||||||
Shotgun,
|
|
||||||
Cap,
|
|
||||||
Rocket,
|
|
||||||
Dart, // Placeholder
|
|
||||||
Grenade,
|
|
||||||
Energy,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|
||||||
{
|
|
||||||
public sealed partial class AmmoComponentData : ISerializationHooks
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class RangedMagazineComponent : Component
|
|
||||||
{
|
|
||||||
public readonly Stack<EntityUid> SpawnedAmmo = new();
|
|
||||||
public Container AmmoContainer = default!;
|
|
||||||
|
|
||||||
public int ShotsLeft => SpawnedAmmo.Count + UnspawnedCount;
|
|
||||||
public int Capacity => _capacity;
|
|
||||||
[DataField("capacity")]
|
|
||||||
private int _capacity = 20;
|
|
||||||
|
|
||||||
public MagazineType MagazineType => _magazineType;
|
|
||||||
[DataField("magazineType")]
|
|
||||||
private MagazineType _magazineType = MagazineType.Unspecified;
|
|
||||||
public BallisticCaliber Caliber => _caliber;
|
|
||||||
[DataField("caliber")]
|
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
// If there's anything already in the magazine
|
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? FillPrototype;
|
|
||||||
|
|
||||||
// By default the magazine won't spawn the entity until needed so we need to keep track of how many left we can spawn
|
|
||||||
// Generally you probablt don't want to use this
|
|
||||||
public int UnspawnedCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Used to load certain ranged weapons quickly
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class SpeedLoaderComponent : Component
|
|
||||||
{
|
|
||||||
[DataField("caliber")] public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
|
||||||
public int Capacity => _capacity;
|
|
||||||
[DataField("capacity")]
|
|
||||||
private int _capacity = 6;
|
|
||||||
|
|
||||||
public Container AmmoContainer = default!;
|
|
||||||
public Stack<EntityUid> SpawnedAmmo = new();
|
|
||||||
public int UnspawnedCount;
|
|
||||||
|
|
||||||
public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount;
|
|
||||||
|
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? FillPrototype;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using Content.Server.PowerCell;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
|
|
||||||
public sealed class BatteryBarrelComponent : ServerRangedBarrelComponent
|
|
||||||
{
|
|
||||||
// The minimum change we need before we can fire
|
|
||||||
[DataField("lowerChargeLimit")]
|
|
||||||
[ViewVariables]
|
|
||||||
public float LowerChargeLimit = 10;
|
|
||||||
|
|
||||||
[DataField("fireCost")]
|
|
||||||
[ViewVariables]
|
|
||||||
public int BaseFireCost = 300;
|
|
||||||
|
|
||||||
// What gets fired
|
|
||||||
[DataField("ammoPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
[ViewVariables]
|
|
||||||
public string? AmmoPrototype;
|
|
||||||
|
|
||||||
public ContainerSlot AmmoContainer = default!;
|
|
||||||
|
|
||||||
public override int ShotsLeft
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) Math.Ceiling(battery.CurrentCharge / BaseFireCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int Capacity
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) Math.Ceiling(battery.MaxCharge / BaseFireCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Shotguns mostly
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
|
|
||||||
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent
|
|
||||||
{
|
|
||||||
// Originally I had this logic shared with PumpBarrel and used a couple of variables to control things
|
|
||||||
// but it felt a lot messier to play around with, especially when adding verbs
|
|
||||||
|
|
||||||
public override int ShotsLeft
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
|
|
||||||
return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public override int Capacity => _capacity;
|
|
||||||
|
|
||||||
[DataField("capacity")]
|
|
||||||
internal int _capacity = 6;
|
|
||||||
|
|
||||||
public ContainerSlot ChamberContainer = default!;
|
|
||||||
public Stack<EntityUid> SpawnedAmmo = default!;
|
|
||||||
public Container AmmoContainer = default!;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("caliber")]
|
|
||||||
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? FillPrototype;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
public int UnspawnedCount;
|
|
||||||
|
|
||||||
public bool BoltOpen
|
|
||||||
{
|
|
||||||
get => _boltOpen;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_boltOpen == value)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gunSystem = EntitySystem.Get<GunSystem>();
|
|
||||||
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
gunSystem.TryEjectChamber(this);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
gunSystem.TryFeedChamber(this);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
_boltOpen = value;
|
|
||||||
gunSystem.UpdateBoltAppearance(this);
|
|
||||||
Dirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private bool _boltOpen;
|
|
||||||
|
|
||||||
[DataField("autoCycle")] public bool AutoCycle;
|
|
||||||
|
|
||||||
// Sounds
|
|
||||||
[DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
|
|
||||||
[DataField("soundBoltOpen")]
|
|
||||||
private SoundSpecifier _soundBoltOpen = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_open.ogg");
|
|
||||||
[DataField("soundBoltClosed")]
|
|
||||||
private SoundSpecifier _soundBoltClosed = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg");
|
|
||||||
[DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
|
|
||||||
public sealed class MagazineBarrelComponent : ServerRangedBarrelComponent
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntityManager _entities = default!;
|
|
||||||
|
|
||||||
[ViewVariables] public ContainerSlot ChamberContainer = default!;
|
|
||||||
[ViewVariables] public bool HasMagazine => MagazineContainer.ContainedEntity != null;
|
|
||||||
public ContainerSlot MagazineContainer = default!;
|
|
||||||
|
|
||||||
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
|
|
||||||
[DataField("magazineTypes")]
|
|
||||||
private MagazineType _magazineTypes = default;
|
|
||||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
|
||||||
[DataField("caliber")]
|
|
||||||
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
public override int ShotsLeft
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var count = 0;
|
|
||||||
if (ChamberContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
|
|
||||||
{
|
|
||||||
count += _entities.GetComponent<RangedMagazineComponent>(magazine).ShotsLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int Capacity
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
// Chamber
|
|
||||||
var count = 1;
|
|
||||||
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
|
|
||||||
{
|
|
||||||
count += _entities.GetComponent<RangedMagazineComponent>(magazine).Capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataField("magFillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? MagFillPrototype;
|
|
||||||
|
|
||||||
public bool BoltOpen
|
|
||||||
{
|
|
||||||
get => _boltOpen;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_boltOpen == value)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gunSystem = EntitySystem.Get<GunSystem>();
|
|
||||||
|
|
||||||
if (value)
|
|
||||||
{
|
|
||||||
gunSystem.TryEjectChamber(this);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), SoundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
gunSystem.TryFeedChamber(this);
|
|
||||||
SoundSystem.Play(Filter.Pvs(Owner), SoundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
_boltOpen = value;
|
|
||||||
gunSystem.UpdateMagazineAppearance(this);
|
|
||||||
Dirty(_entities);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private bool _boltOpen = true;
|
|
||||||
|
|
||||||
[DataField("autoEjectMag")] public bool AutoEjectMag;
|
|
||||||
// If the bolt needs to be open before we can insert / remove the mag (i.e. for LMGs)
|
|
||||||
public bool MagNeedsOpenBolt => _magNeedsOpenBolt;
|
|
||||||
[DataField("magNeedsOpenBolt")]
|
|
||||||
private bool _magNeedsOpenBolt = default;
|
|
||||||
|
|
||||||
// Sounds
|
|
||||||
[DataField("soundBoltOpen", required: true)]
|
|
||||||
public SoundSpecifier SoundBoltOpen = default!;
|
|
||||||
[DataField("soundBoltClosed", required: true)]
|
|
||||||
public SoundSpecifier SoundBoltClosed = default!;
|
|
||||||
[DataField("soundRack", required: true)]
|
|
||||||
public SoundSpecifier SoundRack = default!;
|
|
||||||
[DataField("soundMagInsert", required: true)]
|
|
||||||
public SoundSpecifier SoundMagInsert = default!;
|
|
||||||
[DataField("soundMagEject", required: true)]
|
|
||||||
public SoundSpecifier SoundMagEject = default!;
|
|
||||||
[DataField("soundAutoEject")] public SoundSpecifier SoundAutoEject = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum MagazineType
|
|
||||||
{
|
|
||||||
Unspecified = 0,
|
|
||||||
LPistol = 1 << 0, // Placeholder?
|
|
||||||
Pistol = 1 << 1,
|
|
||||||
HCPistol = 1 << 2,
|
|
||||||
Smg = 1 << 3,
|
|
||||||
SmgTopMounted = 1 << 4,
|
|
||||||
Rifle = 1 << 5,
|
|
||||||
IH = 1 << 6, // Placeholder?
|
|
||||||
Box = 1 << 7,
|
|
||||||
Pan = 1 << 8,
|
|
||||||
Dart = 1 << 9, // Placeholder
|
|
||||||
CalicoTopMounted = 1 << 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Bolt-action rifles
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
|
|
||||||
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
|
|
||||||
{
|
|
||||||
public override int ShotsLeft
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
|
|
||||||
return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int DefaultCapacity = 6;
|
|
||||||
[DataField("capacity")]
|
|
||||||
public override int Capacity { get; } = DefaultCapacity;
|
|
||||||
|
|
||||||
// Even a point having a chamber? I guess it makes some of the below code cleaner
|
|
||||||
public ContainerSlot ChamberContainer = default!;
|
|
||||||
public Stack<EntityUid> SpawnedAmmo = new(DefaultCapacity - 1);
|
|
||||||
public Container AmmoContainer = default!;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("caliber")]
|
|
||||||
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? FillPrototype;
|
|
||||||
|
|
||||||
[ViewVariables] public int UnspawnedCount;
|
|
||||||
|
|
||||||
[DataField("manualCycle")] public bool ManualCycle = true;
|
|
||||||
|
|
||||||
// Sounds
|
|
||||||
[DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
|
|
||||||
|
|
||||||
[DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
|
|
||||||
|
|
||||||
void ISerializationHooks.AfterDeserialization()
|
|
||||||
{
|
|
||||||
SpawnedAmmo = new Stack<EntityUid>(Capacity - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
|
|
||||||
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
|
|
||||||
{
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("caliber")]
|
|
||||||
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
|
|
||||||
|
|
||||||
public Container AmmoContainer = default!;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
public int CurrentSlot;
|
|
||||||
|
|
||||||
public override int Capacity => AmmoSlots.Length;
|
|
||||||
|
|
||||||
[DataField("capacity")]
|
|
||||||
private int _serializedCapacity = 6;
|
|
||||||
|
|
||||||
[DataField("ammoSlots", readOnly: true)]
|
|
||||||
public EntityUid?[] AmmoSlots = Array.Empty<EntityUid?>();
|
|
||||||
|
|
||||||
public override int ShotsLeft => AmmoContainer.ContainedEntities.Count;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
|
||||||
public string? FillPrototype;
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
public int UnspawnedCount;
|
|
||||||
|
|
||||||
// Sounds
|
|
||||||
[DataField("soundEject")]
|
|
||||||
public SoundSpecifier SoundEject = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
|
|
||||||
|
|
||||||
[DataField("soundInsert")]
|
|
||||||
public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
|
|
||||||
|
|
||||||
[DataField("soundSpin")]
|
|
||||||
public SoundSpecifier SoundSpin = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/revolver_spin.ogg");
|
|
||||||
|
|
||||||
void ISerializationHooks.BeforeSerialization()
|
|
||||||
{
|
|
||||||
_serializedCapacity = AmmoSlots.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ISerializationHooks.AfterDeserialization()
|
|
||||||
{
|
|
||||||
AmmoSlots = new EntityUid?[_serializedCapacity];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
using Content.Shared.Sound;
|
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// All of the ranged weapon components inherit from this to share mechanics like shooting etc.
|
|
||||||
/// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.)
|
|
||||||
/// </summary>
|
|
||||||
[Friend(typeof(GunSystem))]
|
|
||||||
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, ISerializationHooks
|
|
||||||
{
|
|
||||||
public override FireRateSelector FireRateSelector => _fireRateSelector;
|
|
||||||
|
|
||||||
[DataField("currentSelector")]
|
|
||||||
private FireRateSelector _fireRateSelector = FireRateSelector.Safety;
|
|
||||||
|
|
||||||
public override FireRateSelector AllRateSelectors => _fireRateSelector;
|
|
||||||
|
|
||||||
[DataField("fireRate")]
|
|
||||||
public override float FireRate { get; } = 2f;
|
|
||||||
|
|
||||||
// _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
|
|
||||||
public TimeSpan LastFire;
|
|
||||||
|
|
||||||
// Recoil / spray control
|
|
||||||
[DataField("minAngle")]
|
|
||||||
private float _minAngleDegrees;
|
|
||||||
|
|
||||||
public Angle MinAngle { get; private set; }
|
|
||||||
|
|
||||||
[DataField("maxAngle")]
|
|
||||||
private float _maxAngleDegrees = 45;
|
|
||||||
|
|
||||||
public Angle MaxAngle { get; private set; }
|
|
||||||
|
|
||||||
public Angle CurrentAngle = Angle.Zero;
|
|
||||||
|
|
||||||
[DataField("angleDecay")]
|
|
||||||
private float _angleDecayDegrees = 20;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How slowly the angle's theta decays per second in radians
|
|
||||||
/// </summary>
|
|
||||||
public float AngleDecay { get; private set; }
|
|
||||||
|
|
||||||
[DataField("angleIncrease")]
|
|
||||||
private float? _angleIncreaseDegrees;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How quickly the angle's theta builds for every shot fired in radians
|
|
||||||
/// </summary>
|
|
||||||
public float AngleIncrease { get; private set; }
|
|
||||||
|
|
||||||
// Multiplies the ammo spread to get the final spread of each pellet
|
|
||||||
[DataField("ammoSpreadRatio")]
|
|
||||||
public float SpreadRatio { get; private set; }
|
|
||||||
|
|
||||||
[DataField("canMuzzleFlash")]
|
|
||||||
public bool CanMuzzleFlash { get; } = true;
|
|
||||||
|
|
||||||
// Sounds
|
|
||||||
[DataField("soundGunshot", required: true)]
|
|
||||||
public SoundSpecifier SoundGunshot { get; set; } = default!;
|
|
||||||
|
|
||||||
[DataField("soundEmpty")]
|
|
||||||
public SoundSpecifier SoundEmpty { get; } = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg");
|
|
||||||
|
|
||||||
void ISerializationHooks.BeforeSerialization()
|
|
||||||
{
|
|
||||||
_minAngleDegrees = (float) (MinAngle.Degrees * 2);
|
|
||||||
_maxAngleDegrees = (float) (MaxAngle.Degrees * 2);
|
|
||||||
_angleIncreaseDegrees = MathF.Round(AngleIncrease / ((float) Math.PI / 180f), 2);
|
|
||||||
AngleDecay = MathF.Round(AngleDecay / ((float) Math.PI / 180f), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ISerializationHooks.AfterDeserialization()
|
|
||||||
{
|
|
||||||
// This hard-to-read area's dealing with recoil
|
|
||||||
// Use degrees in yaml as it's easier to read compared to "0.0125f"
|
|
||||||
MinAngle = Angle.FromDegrees(_minAngleDegrees / 2f);
|
|
||||||
|
|
||||||
// Random doubles it as it's +/- so uhh we'll just half it here for readability
|
|
||||||
MaxAngle = Angle.FromDegrees(_maxAngleDegrees / 2f);
|
|
||||||
|
|
||||||
_angleIncreaseDegrees ??= 40 / FireRate;
|
|
||||||
AngleIncrease = _angleIncreaseDegrees.Value * (float) Math.PI / 180f;
|
|
||||||
|
|
||||||
AngleDecay = _angleDecayDegrees * (float) Math.PI / 180f;
|
|
||||||
|
|
||||||
// For simplicity we'll enforce it this way; ammo determines max spread
|
|
||||||
if (SpreadRatio > 1.0f)
|
|
||||||
{
|
|
||||||
Logger.Error("SpreadRatio must be <= 1.0f for guns");
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Raised on a gun when it fires projectiles.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GunShotEvent : EntityEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Uid of the entity that shot.
|
|
||||||
/// </summary>
|
|
||||||
public EntityUid Uid;
|
|
||||||
|
|
||||||
public readonly EntityUid[] FiredProjectiles;
|
|
||||||
|
|
||||||
public GunShotEvent(EntityUid[] firedProjectiles)
|
|
||||||
{
|
|
||||||
FiredProjectiles = firedProjectiles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Raised on ammo when it is fired.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class AmmoShotEvent : EntityEventArgs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Uid of the entity that shot.
|
|
||||||
/// </summary>
|
|
||||||
public EntityUid Uid;
|
|
||||||
|
|
||||||
public readonly EntityUid[] FiredProjectiles;
|
|
||||||
|
|
||||||
public AmmoShotEvent(EntityUid[] firedProjectiles)
|
|
||||||
{
|
|
||||||
FiredProjectiles = firedProjectiles;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Components;
|
||||||
|
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class AmmoCounterComponent : SharedAmmoCounterComponent {}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Content.Server.Weapon.Ranged.Ammunition.Components
|
namespace Content.Server.Weapon.Ranged.Components
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed class ChemicalAmmoComponent : Component
|
public sealed class ChemicalAmmoComponent : Component
|
||||||
@@ -2,7 +2,7 @@ using Content.Shared.Damage.Prototypes;
|
|||||||
using Content.Shared.Sound;
|
using Content.Shared.Sound;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
namespace Content.Server.Weapon.Ranged.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plays the specified sound upon receiving damage of that type.
|
/// Plays the specified sound upon receiving damage of that type.
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
using Content.Shared.Weapons.Ranged;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void OnAmmoExamine(EntityUid uid, AmmoComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
var text = Loc.GetString("ammo-component-on-examine",("caliber", component.Caliber));
|
|
||||||
args.PushMarkup(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeBullet(AmmoComponent component, EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (component.AmmoIsProjectile)
|
|
||||||
{
|
|
||||||
return component.Owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.Spent)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
component.Spent = true;
|
|
||||||
|
|
||||||
if (TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.Spent, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var entity = EntityManager.SpawnEntity(component.ProjectileId, spawnAt);
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MuzzleFlash(EntityUid entity, AmmoComponent component, Angle angle)
|
|
||||||
{
|
|
||||||
if (component.MuzzleFlashSprite == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var time = _gameTiming.CurTime;
|
|
||||||
var deathTime = time + TimeSpan.FromMilliseconds(200);
|
|
||||||
// Offset the sprite so it actually looks like it's coming from the gun
|
|
||||||
var offset = new Vector2(0.0f, -0.5f);
|
|
||||||
|
|
||||||
var message = new EffectSystemMessage
|
|
||||||
{
|
|
||||||
EffectSprite = component.MuzzleFlashSprite.ToString(),
|
|
||||||
Born = time,
|
|
||||||
DeathTime = deathTime,
|
|
||||||
AttachedEntityUid = entity,
|
|
||||||
AttachedOffset = offset,
|
|
||||||
//Rotated from east facing
|
|
||||||
Rotation = -MathF.PI / 2f,
|
|
||||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
|
|
||||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
|
||||||
Shaded = false
|
|
||||||
};
|
|
||||||
|
|
||||||
/* TODO: Fix rotation when shooting sideways. This was the closest I got but still had issues.
|
|
||||||
* var time = _gameTiming.CurTime;
|
|
||||||
var deathTime = time + TimeSpan.FromMilliseconds(200);
|
|
||||||
var entityRotation = EntityManager.GetComponent<TransformComponent>(entity).WorldRotation;
|
|
||||||
var localAngle = entityRotation - (angle + MathF.PI / 2f);
|
|
||||||
// Offset the sprite so it actually looks like it's coming from the gun
|
|
||||||
var offset = localAngle.RotateVec(new Vector2(0.0f, -0.5f));
|
|
||||||
|
|
||||||
var message = new EffectSystemMessage
|
|
||||||
{
|
|
||||||
EffectSprite = component.MuzzleFlashSprite.ToString(),
|
|
||||||
Born = time,
|
|
||||||
DeathTime = deathTime,
|
|
||||||
AttachedEntityUid = entity,
|
|
||||||
AttachedOffset = offset,
|
|
||||||
//Rotated from east facing
|
|
||||||
Rotation = (float) (localAngle - MathF.PI / 2),
|
|
||||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
|
|
||||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
|
||||||
Shaded = false
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
_effects.CreateParticle(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
// Probably needs combining with magazines in future given the common functionality.
|
|
||||||
|
|
||||||
private void OnAmmoBoxAltVerbs(EntityUid uid, AmmoBoxComponent component, GetVerbsEvent<AlternativeVerb> args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (component.AmmoLeft == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AlternativeVerb verb = new()
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("dump-vert-get-data-text"),
|
|
||||||
IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png",
|
|
||||||
Act = () => AmmoBoxEjectContents(component, 10)
|
|
||||||
};
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAmmoBoxInteractHand(EntityUid uid, AmmoBoxComponent component, InteractHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
TryUse(args.User, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAmmoBoxUse(EntityUid uid, AmmoBoxComponent component, UseInHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
TryUse(args.User, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAmmoBoxInteractUsing(EntityUid uid, AmmoBoxComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (TryComp(args.Used, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
if (TryInsertAmmo(args.User, args.Used, component, ammoComponent))
|
|
||||||
{
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryComp(args.Used, out RangedMagazineComponent? rangedMagazine)) return;
|
|
||||||
|
|
||||||
for (var i = 0; i < Math.Max(10, rangedMagazine.ShotsLeft); i++)
|
|
||||||
{
|
|
||||||
if (TakeAmmo(rangedMagazine) is not {Valid: true} ammo)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryInsertAmmo(args.User, ammo, component))
|
|
||||||
{
|
|
||||||
TryInsertAmmo(args.User, ammo, rangedMagazine);
|
|
||||||
args.Handled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAmmoBoxInit(EntityUid uid, AmmoBoxComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.Name}-container", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
component.SpawnedAmmo.Push(entity);
|
|
||||||
component.AmmoContainer.Insert(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAmmoBoxExamine(EntityUid uid, AmmoBoxComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
args.PushMarkup(Loc.GetString("ammo-box-component-on-examine-caliber-description", ("caliber", component.Caliber)));
|
|
||||||
args.PushMarkup(Loc.GetString("ammo-box-component-on-examine-remaining-ammo-description", ("ammoLeft", component.AmmoLeft),("capacity", component.Capacity)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAmmoBoxMapInit(EntityUid uid, AmmoBoxComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount += component.Capacity;
|
|
||||||
UpdateAmmoBoxAppearance(uid, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAmmoBoxAppearance(EntityUid uid, AmmoBoxComponent ammoBox, AppearanceComponent? appearanceComponent = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(uid, ref appearanceComponent, false)) return;
|
|
||||||
|
|
||||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, ammoBox.AmmoLeft);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, ammoBox.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AmmoBoxEjectContents(AmmoBoxComponent ammoBox, int count)
|
|
||||||
{
|
|
||||||
var ejectCount = Math.Min(count, ammoBox.Capacity);
|
|
||||||
var ejectAmmo = new List<EntityUid>(ejectCount);
|
|
||||||
|
|
||||||
for (var i = 0; i < Math.Min(count, ammoBox.Capacity); i++)
|
|
||||||
{
|
|
||||||
if (TakeAmmo(ammoBox) is not { } ammo)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ejectAmmo.Add(ammo);
|
|
||||||
}
|
|
||||||
|
|
||||||
EjectCasings(ejectAmmo);
|
|
||||||
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryUse(EntityUid user, AmmoBoxComponent ammoBox)
|
|
||||||
{
|
|
||||||
if (!TryComp(user, out HandsComponent? handsComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TakeAmmo(ammoBox) is not { } ammo)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_handsSystem.TryPickup(user, ammo, handsComp: handsComponent))
|
|
||||||
{
|
|
||||||
TryInsertAmmo(user, ammo, ammoBox);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertAmmo(EntityUid user, EntityUid ammo, AmmoBoxComponent ammoBox, AmmoComponent? ammoComponent = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(ammo, ref ammoComponent, false))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != ammoBox.Caliber)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("ammo-box-component-try-insert-ammo-wrong-caliber"), ammo, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoBox.AmmoLeft >= ammoBox.Capacity)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("ammo-box-component-try-insert-ammo-no-room"), ammo, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ammoBox.SpawnedAmmo.Push(ammo);
|
|
||||||
ammoBox.AmmoContainer.Insert(ammo);
|
|
||||||
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeAmmo(AmmoBoxComponent ammoBox, TransformComponent? xform = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(ammoBox.Owner, ref xform)) return null;
|
|
||||||
|
|
||||||
if (ammoBox.SpawnedAmmo.TryPop(out var ammo))
|
|
||||||
{
|
|
||||||
ammoBox.AmmoContainer.Remove(ammo);
|
|
||||||
return ammo;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoBox.UnspawnedCount > 0)
|
|
||||||
{
|
|
||||||
ammo = EntityManager.SpawnEntity(ammoBox.FillPrototype, xform.Coordinates);
|
|
||||||
|
|
||||||
// when dumping from held ammo box, this detaches the spawned ammo from the player.
|
|
||||||
EntityManager.GetComponent<TransformComponent>(ammo).AttachParentToContainerOrGrid();
|
|
||||||
|
|
||||||
ammoBox.UnspawnedCount--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ammo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
using Content.Server.Projectiles.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.PowerCell.Components;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void OnBatteryInit(EntityUid uid, BatteryBarrelComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
if (component.AmmoPrototype != null)
|
|
||||||
{
|
|
||||||
component.AmmoContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-ammo-container");
|
|
||||||
}
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBatteryMapInit(EntityUid uid, BatteryBarrelComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
UpdateBatteryAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCellSlotUpdated(EntityUid uid, BatteryBarrelComponent component, PowerCellChangedEvent args)
|
|
||||||
{
|
|
||||||
UpdateBatteryAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateBatteryAppearance(BatteryBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (!EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
|
||||||
|
|
||||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, _cell.TryGetBatteryFromSlot(component.Owner, out _));
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? PeekAmmo(BatteryBarrelComponent component)
|
|
||||||
{
|
|
||||||
// Spawn a dummy entity because it's easier to work with I guess
|
|
||||||
// This will get re-used for the projectile
|
|
||||||
var ammo = component.AmmoContainer.ContainedEntity;
|
|
||||||
if (ammo == null)
|
|
||||||
{
|
|
||||||
ammo = EntityManager.SpawnEntity(component.AmmoPrototype, Transform(component.Owner).Coordinates);
|
|
||||||
component.AmmoContainer.Insert(ammo.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ammo.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeProjectile(BatteryBarrelComponent component, EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (!_cell.TryGetBatteryFromSlot(component.Owner, out var capacitor))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (capacitor.CurrentCharge < component.LowerChargeLimit)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Can fire confirmed
|
|
||||||
// Multiply the entity's damage / whatever by the percentage of charge the shot has.
|
|
||||||
EntityUid? entity;
|
|
||||||
var chargeChange = Math.Min(capacitor.CurrentCharge, component.BaseFireCost);
|
|
||||||
if (capacitor.UseCharge(chargeChange) < component.LowerChargeLimit)
|
|
||||||
{
|
|
||||||
// Handling of funny exploding cells.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var energyRatio = chargeChange / component.BaseFireCost;
|
|
||||||
|
|
||||||
if (component.AmmoContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
entity = component.AmmoContainer.ContainedEntity;
|
|
||||||
component.AmmoContainer.Remove(entity.Value);
|
|
||||||
Transform(entity.Value).Coordinates = spawnAt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
entity = EntityManager.SpawnEntity(component.AmmoPrototype, spawnAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryComp(entity.Value, out ProjectileComponent? projectileComponent))
|
|
||||||
{
|
|
||||||
if (energyRatio < 1.0)
|
|
||||||
{
|
|
||||||
projectileComponent.Damage *= energyRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (TryComp(entity.Value, out HitscanComponent? hitscanComponent))
|
|
||||||
{
|
|
||||||
hitscanComponent.Damage *= energyRatio;
|
|
||||||
hitscanComponent.ColorModifier = energyRatio;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
|
|
||||||
}
|
|
||||||
|
|
||||||
// capacitor.UseCharge() triggers a PowerCellChangedEvent which will cause appearance to be updated.
|
|
||||||
// So let's not double-call UpdateAppearance() here.
|
|
||||||
return entity.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void AddToggleBoltVerb(EntityUid uid, BoltActionBarrelComponent component, GetVerbsEvent<InteractionVerb> args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null ||
|
|
||||||
!args.CanAccess ||
|
|
||||||
!args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
InteractionVerb verb = new()
|
|
||||||
{
|
|
||||||
Text = component.BoltOpen
|
|
||||||
? Loc.GetString("close-bolt-verb-get-data-text")
|
|
||||||
: Loc.GetString("open-bolt-verb-get-data-text"),
|
|
||||||
Act = () => component.BoltOpen = !component.BoltOpen
|
|
||||||
};
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBoltExamine(EntityUid uid, BoltActionBarrelComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
args.PushMarkup(Loc.GetString("bolt-action-barrel-component-on-examine", ("caliber", component.Caliber)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBoltFireAttempt(EntityUid uid, BoltActionBarrelComponent component, GunFireAttemptEvent args)
|
|
||||||
{
|
|
||||||
if (args.Cancelled) return;
|
|
||||||
|
|
||||||
if (component.BoltOpen || component.ChamberContainer.ContainedEntity == null)
|
|
||||||
args.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBoltMapInit(EntityUid uid, BoltActionBarrelComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
if (component.FillPrototype != null)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount += component.Capacity;
|
|
||||||
if (component.UnspawnedCount > 0)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
var chamberEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
|
|
||||||
component.ChamberContainer.Insert(chamberEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateBoltAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateBoltAppearance(BoltActionBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
|
||||||
|
|
||||||
appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBoltInit(EntityUid uid, BoltActionBarrelComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
component.SpawnedAmmo = new Stack<EntityUid>(component.Capacity - 1);
|
|
||||||
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-ammo-container", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
component.SpawnedAmmo.Push(entity);
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component.ChamberContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber-container");
|
|
||||||
|
|
||||||
if (TryComp(uid, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateBoltAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBoltUse(EntityUid uid, BoltActionBarrelComponent component, UseInHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
|
|
||||||
if (component.BoltOpen)
|
|
||||||
{
|
|
||||||
component.BoltOpen = false;
|
|
||||||
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-closed"), uid, Filter.Entities(args.User));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CycleBolt(component, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CycleBolt(BoltActionBarrelComponent component, bool manual = false)
|
|
||||||
{
|
|
||||||
TryEjectChamber(component);
|
|
||||||
TryFeedChamber(component);
|
|
||||||
|
|
||||||
if (component.ChamberContainer.ContainedEntity == null && manual)
|
|
||||||
{
|
|
||||||
component.BoltOpen = true;
|
|
||||||
|
|
||||||
if (_container.TryGetContainingContainer(component.Owner, out var container))
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-opened"), container.Owner, Filter.Entities(container.Owner));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateBoltAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryEjectChamber(BoltActionBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (component.ChamberContainer.ContainedEntity is {Valid: true} chambered)
|
|
||||||
{
|
|
||||||
if (!component.ChamberContainer.Remove(chambered))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (TryComp(chambered, out AmmoComponent? ammoComponent) && !ammoComponent.Caseless)
|
|
||||||
EjectCasing(chambered);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryFeedChamber(BoltActionBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (component.ChamberContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (component.SpawnedAmmo.TryPop(out var next))
|
|
||||||
{
|
|
||||||
component.AmmoContainer.Remove(next);
|
|
||||||
component.ChamberContainer.Insert(next);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (component.UnspawnedCount > 0)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent<TransformComponent>(component.Owner).Coordinates);
|
|
||||||
component.ChamberContainer.Insert(ammoEntity);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBoltInteractUsing(EntityUid uid, BoltActionBarrelComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (TryInsertBullet(args.User, args.Used, component))
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertBullet(EntityUid user, EntityUid ammo, BoltActionBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(ammo, out AmmoComponent? ammoComponent))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != component.Caliber)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!component.BoltOpen)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-bolt-closed"), component.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.ChamberContainer.ContainedEntity == null)
|
|
||||||
{
|
|
||||||
component.ChamberContainer.Insert(ammo);
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateBoltAppearance(component);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
|
|
||||||
{
|
|
||||||
component.AmmoContainer.Insert(ammo);
|
|
||||||
component.SpawnedAmmo.Push(ammo);
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateBoltAppearance(component);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(user));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBoltGetState(EntityUid uid, BoltActionBarrelComponent component, ref ComponentGetState args)
|
|
||||||
{
|
|
||||||
(int, int)? count = (component.ShotsLeft, component.Capacity);
|
|
||||||
var chamberedExists = component.ChamberContainer.ContainedEntity != null;
|
|
||||||
// (Is one chambered?, is the bullet spend)
|
|
||||||
var chamber = (chamberedExists, false);
|
|
||||||
|
|
||||||
if (chamberedExists && TryComp<AmmoComponent?>(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
|
|
||||||
{
|
|
||||||
chamber.Item2 = ammo.Spent;
|
|
||||||
}
|
|
||||||
|
|
||||||
args.State = new BoltActionBarrelComponentState(
|
|
||||||
chamber,
|
|
||||||
component.FireRateSelector,
|
|
||||||
count,
|
|
||||||
component.SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? PeekAmmo(BoltActionBarrelComponent component)
|
|
||||||
{
|
|
||||||
return component.ChamberContainer.ContainedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeProjectile(BoltActionBarrelComponent component, EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (component.AutoCycle)
|
|
||||||
{
|
|
||||||
CycleBolt(component);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
|
|
||||||
|
|
||||||
var ammoComponent = EntityManager.GetComponentOrNull<AmmoComponent>(chamberEntity);
|
|
||||||
|
|
||||||
return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using Content.Server.CombatMode;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Interaction.Components;
|
|
||||||
using Content.Server.Projectiles.Components;
|
|
||||||
using Content.Server.Weapon.Melee;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Audio;
|
|
||||||
using Content.Shared.Camera;
|
|
||||||
using Content.Shared.Damage;
|
|
||||||
using Content.Shared.Database;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Sound;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Physics;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void OnMeleeAttempt(EntityUid uid, ServerRangedWeaponComponent component, ref MeleeAttackAttemptEvent args)
|
|
||||||
{
|
|
||||||
args.Cancelled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to fire a round of ammo out of the weapon.
|
|
||||||
/// </summary>
|
|
||||||
private void TryFire(EntityUid user, EntityCoordinates targetCoords, ServerRangedWeaponComponent gun)
|
|
||||||
{
|
|
||||||
if (!TryComp(gun.Owner, out ServerRangedBarrelComponent? barrel)) return;
|
|
||||||
|
|
||||||
if (!TryComp(user, out HandsComponent? hands) || hands.ActiveHand?.HeldEntity != gun.Owner) return;
|
|
||||||
|
|
||||||
if (!TryComp(user, out CombatModeComponent? combat) ||
|
|
||||||
!combat.IsInCombatMode ||
|
|
||||||
!_blocker.CanInteract(user, gun.Owner)) return;
|
|
||||||
|
|
||||||
var fireAttempt = new GunFireAttemptEvent(user, gun);
|
|
||||||
EntityManager.EventBus.RaiseLocalEvent(gun.Owner, fireAttempt);
|
|
||||||
|
|
||||||
if (fireAttempt.Cancelled) return;
|
|
||||||
|
|
||||||
var curTime = _gameTiming.CurTime;
|
|
||||||
var span = curTime - gun.LastFireTime;
|
|
||||||
if (span.TotalSeconds < 1 / barrel.FireRate) return;
|
|
||||||
|
|
||||||
// TODO: Clumsy should be eventbus I think?
|
|
||||||
|
|
||||||
gun.LastFireTime = curTime;
|
|
||||||
var coordinates = Transform(gun.Owner).Coordinates;
|
|
||||||
|
|
||||||
if (gun.ClumsyCheck && EntityManager.TryGetComponent<ClumsyComponent>(user, out var clumsyComponent) && ClumsyComponent.TryRollClumsy(user, gun.ClumsyExplodeChance))
|
|
||||||
{
|
|
||||||
//Wound them
|
|
||||||
_damageable.TryChangeDamage(user, clumsyComponent.ClumsyDamage);
|
|
||||||
_stun.TryParalyze(user, TimeSpan.FromSeconds(3f), true);
|
|
||||||
|
|
||||||
// Apply salt to the wound ("Honk!")
|
|
||||||
SoundSystem.Play(
|
|
||||||
Filter.Pvs(gun.Owner), gun.ClumsyWeaponHandlingSound.GetSound(),
|
|
||||||
coordinates, AudioParams.Default.WithMaxDistance(5));
|
|
||||||
|
|
||||||
SoundSystem.Play(
|
|
||||||
Filter.Pvs(gun.Owner), gun.ClumsyWeaponShotSound.GetSound(),
|
|
||||||
coordinates, AudioParams.Default.WithMaxDistance(5));
|
|
||||||
|
|
||||||
user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy"));
|
|
||||||
|
|
||||||
EntityManager.DeleteEntity(gun.Owner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Firing confirmed
|
|
||||||
|
|
||||||
if (gun.CanHotspot)
|
|
||||||
_atmos.HotspotExpose(coordinates, 700, 50);
|
|
||||||
|
|
||||||
EntityManager.EventBus.RaiseLocalEvent(gun.Owner, new GunShotEvent());
|
|
||||||
Fire(user, barrel, targetCoords);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fires a round of ammo out of the weapon.
|
|
||||||
/// </summary>
|
|
||||||
private void Fire(EntityUid shooter, ServerRangedBarrelComponent component, EntityCoordinates coordinates)
|
|
||||||
{
|
|
||||||
if (component.ShotsLeft == 0)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ammo = PeekAtAmmo(component);
|
|
||||||
if (TakeOutProjectile(component, Transform(shooter).Coordinates) is not {Valid: true} projectile)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetPos = coordinates.ToMapPos(EntityManager);
|
|
||||||
|
|
||||||
// At this point firing is confirmed
|
|
||||||
var direction = (targetPos - Transform(shooter).WorldPosition).ToAngle();
|
|
||||||
var angle = GetRecoilAngle(component, direction);
|
|
||||||
// This should really be client-side but for now we'll just leave it here
|
|
||||||
if (HasComp<CameraRecoilComponent>(shooter))
|
|
||||||
{
|
|
||||||
var kick = -angle.ToVec() * 0.15f;
|
|
||||||
_recoil.KickCamera(shooter, kick);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This section probably needs tweaking so there can be caseless hitscan etc.
|
|
||||||
if (TryComp(projectile, out HitscanComponent? hitscan))
|
|
||||||
{
|
|
||||||
FireHitscan(shooter, hitscan, component, angle);
|
|
||||||
}
|
|
||||||
else if (HasComp<ProjectileComponent>(projectile) &&
|
|
||||||
TryComp(ammo, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
FireProjectiles(shooter, projectile, component, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity, ammo!.Value);
|
|
||||||
|
|
||||||
if (component.CanMuzzleFlash)
|
|
||||||
{
|
|
||||||
MuzzleFlash(component.Owner, ammoComponent, angle);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
EntityManager.DeleteEntity(ammo.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Invalid types
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
SoundSystem.Play(Filter.Broadcast(), component.SoundGunshot.GetSound(), component.Owner);
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
component.LastFire = _gameTiming.CurTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Firing
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles firing one or many projectiles
|
|
||||||
/// </summary>
|
|
||||||
private void FireProjectiles(EntityUid shooter, EntityUid baseProjectile, ServerRangedBarrelComponent component, int count, float evenSpreadAngle, Angle angle, float velocity, EntityUid ammo)
|
|
||||||
{
|
|
||||||
List<Angle>? sprayAngleChange = null;
|
|
||||||
if (count > 1)
|
|
||||||
{
|
|
||||||
evenSpreadAngle *= component.SpreadRatio;
|
|
||||||
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
var firedProjectiles = new EntityUid[count];
|
|
||||||
for (var i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
EntityUid projectile;
|
|
||||||
|
|
||||||
if (i == 0)
|
|
||||||
{
|
|
||||||
projectile = baseProjectile;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO: Cursed as bruh
|
|
||||||
projectile = EntityManager.SpawnEntity(
|
|
||||||
MetaData(baseProjectile).EntityPrototype?.ID,
|
|
||||||
Transform(baseProjectile).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
firedProjectiles[i] = projectile;
|
|
||||||
|
|
||||||
Angle projectileAngle;
|
|
||||||
|
|
||||||
if (sprayAngleChange != null)
|
|
||||||
{
|
|
||||||
projectileAngle = angle + sprayAngleChange[i];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
projectileAngle = angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
var physics = EntityManager.GetComponent<IPhysBody>(projectile);
|
|
||||||
physics.BodyStatus = BodyStatus.InAir;
|
|
||||||
|
|
||||||
var projectileComponent = EntityManager.GetComponent<ProjectileComponent>(projectile);
|
|
||||||
projectileComponent.IgnoreEntity(shooter);
|
|
||||||
|
|
||||||
// FIXME: Work around issue where inserting and removing an entity from a container,
|
|
||||||
// then setting its linear velocity in the same tick resets velocity back to zero.
|
|
||||||
// See SharedBroadphaseSystem.HandleContainerInsert()... It sets Awake to false, which causes this.
|
|
||||||
projectile.SpawnTimer(TimeSpan.FromMilliseconds(25), () =>
|
|
||||||
{
|
|
||||||
EntityManager.GetComponent<IPhysBody>(projectile)
|
|
||||||
.LinearVelocity = projectileAngle.ToVec() * velocity;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Transform(projectile).WorldRotation = projectileAngle + MathHelper.PiOver2;
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityManager.EventBus.RaiseLocalEvent(component.Owner, new Barrels.Components.GunShotEvent(firedProjectiles));
|
|
||||||
EntityManager.EventBus.RaiseLocalEvent(ammo, new AmmoShotEvent(firedProjectiles));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
|
|
||||||
/// </summary>
|
|
||||||
private List<Angle> Linspace(double start, double end, int intervals)
|
|
||||||
{
|
|
||||||
DebugTools.Assert(intervals > 1);
|
|
||||||
|
|
||||||
var linspace = new List<Angle>(intervals);
|
|
||||||
|
|
||||||
for (var i = 0; i <= intervals - 1; i++)
|
|
||||||
{
|
|
||||||
linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
|
|
||||||
}
|
|
||||||
return linspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fires hitscan entities and then displays their effects
|
|
||||||
/// </summary>
|
|
||||||
private void FireHitscan(EntityUid shooter, HitscanComponent hitscan, ServerRangedBarrelComponent component, Angle angle)
|
|
||||||
{
|
|
||||||
var ray = new CollisionRay(Transform(component.Owner).WorldPosition, angle.ToVec(), (int) hitscan.CollisionMask);
|
|
||||||
var rayCastResults = _physics.IntersectRay(Transform(component.Owner).MapID, ray, hitscan.MaxLength, shooter, false).ToList();
|
|
||||||
|
|
||||||
if (rayCastResults.Count >= 1)
|
|
||||||
{
|
|
||||||
var result = rayCastResults[0];
|
|
||||||
var distance = result.Distance;
|
|
||||||
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
|
|
||||||
var modifiedDamage = _damageable.TryChangeDamage(result.HitEntity, hitscan.Damage);
|
|
||||||
if (modifiedDamage != null)
|
|
||||||
_adminLogger.Add(LogType.HitScanHit,
|
|
||||||
$"{EntityManager.ToPrettyString(shooter):user} hit {EntityManager.ToPrettyString(result.HitEntity):target} using {EntityManager.ToPrettyString(hitscan.Owner):used} and dealt {modifiedDamage.Total:damage} damage");
|
|
||||||
|
|
||||||
PlaySound(rayCastResults[0].HitEntity, modifiedDamage, hitscan.SoundHit, hitscan.ForceSound);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Impact sounds
|
|
||||||
|
|
||||||
public void PlaySound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
|
|
||||||
{
|
|
||||||
// Like projectiles and melee,
|
|
||||||
// 1. Entity specific sound
|
|
||||||
// 2. Ammo's sound
|
|
||||||
// 3. Nothing
|
|
||||||
var playedSound = false;
|
|
||||||
|
|
||||||
if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.Total > 0 && TryComp<RangedDamageSoundComponent>(otherEntity, out var rangedSound))
|
|
||||||
{
|
|
||||||
var type = MeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, _protoManager);
|
|
||||||
|
|
||||||
if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(
|
|
||||||
Filter.Pvs(otherEntity, entityManager: EntityManager),
|
|
||||||
damageSoundType!.GetSound(),
|
|
||||||
otherEntity,
|
|
||||||
AudioHelpers.WithVariation(DamagePitchVariation));
|
|
||||||
|
|
||||||
playedSound = true;
|
|
||||||
}
|
|
||||||
else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(
|
|
||||||
Filter.Pvs(otherEntity, entityManager: EntityManager),
|
|
||||||
damageSoundGroup!.GetSound(),
|
|
||||||
otherEntity,
|
|
||||||
AudioHelpers.WithVariation(DamagePitchVariation));
|
|
||||||
|
|
||||||
playedSound = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!playedSound && weaponSound != null)
|
|
||||||
SoundSystem.Play(Filter.Pvs(otherEntity, entityManager: EntityManager), weaponSound.GetSound(), otherEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using Content.Shared.Weapons.Ranged;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void AddEjectMagazineVerb(EntityUid uid, MagazineBarrelComponent component, GetVerbsEvent<AlternativeVerb> args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null ||
|
|
||||||
!args.CanAccess ||
|
|
||||||
!args.CanInteract ||
|
|
||||||
component.MagazineContainer.ContainedEntity is not EntityUid mag ||
|
|
||||||
!_blocker.CanPickup(args.User, mag))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (component.MagNeedsOpenBolt && !component.BoltOpen)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AlternativeVerb verb = new()
|
|
||||||
{
|
|
||||||
Text = MetaData(mag).EntityName,
|
|
||||||
Category = VerbCategory.Eject,
|
|
||||||
Act = () => RemoveMagazine(args.User, component)
|
|
||||||
};
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddMagazineInteractionVerbs(EntityUid uid, MagazineBarrelComponent component, GetVerbsEvent<InteractionVerb> args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null ||
|
|
||||||
!args.CanAccess ||
|
|
||||||
!args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Toggle bolt verb
|
|
||||||
InteractionVerb toggleBolt = new()
|
|
||||||
{
|
|
||||||
Text = component.BoltOpen
|
|
||||||
? Loc.GetString("close-bolt-verb-get-data-text")
|
|
||||||
: Loc.GetString("open-bolt-verb-get-data-text"),
|
|
||||||
Act = () => component.BoltOpen = !component.BoltOpen
|
|
||||||
};
|
|
||||||
args.Verbs.Add(toggleBolt);
|
|
||||||
|
|
||||||
// Are we holding a mag that we can insert?
|
|
||||||
if (args.Using is not {Valid: true} @using ||
|
|
||||||
!CanInsertMagazine(args.User, @using, component) ||
|
|
||||||
!_blocker.CanDrop(args.User))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Insert mag verb
|
|
||||||
InteractionVerb insert = new()
|
|
||||||
{
|
|
||||||
Text = MetaData(@using).EntityName,
|
|
||||||
Category = VerbCategory.Insert,
|
|
||||||
Act = () => InsertMagazine(args.User, @using, component)
|
|
||||||
};
|
|
||||||
args.Verbs.Add(insert);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMagazineExamine(EntityUid uid, MagazineBarrelComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine", ("caliber", component.Caliber)));
|
|
||||||
|
|
||||||
foreach (var magazineType in GetMagazineTypes(component))
|
|
||||||
{
|
|
||||||
args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine-magazine-type", ("magazineType", magazineType)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMagazineUse(EntityUid uid, MagazineBarrelComponent component, UseInHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
// Behavior:
|
|
||||||
// If bolt open just close it
|
|
||||||
// If bolt closed then cycle
|
|
||||||
// If we cycle then get next round
|
|
||||||
// If no more round then open bolt
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
|
|
||||||
if (component.BoltOpen)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltClosed.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-use-entity-bolt-closed"), component.Owner, Filter.Entities(args.User));
|
|
||||||
component.BoltOpen = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Could play a rack-slide specific sound here if you're so inclined (if the chamber is empty but rounds are available)
|
|
||||||
|
|
||||||
CycleMagazine(component, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateMagazineAppearance(MagazineBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
|
||||||
|
|
||||||
appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
|
|
||||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, component.MagazineContainer.ContainedEntity != null);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMagazineGetState(EntityUid uid, MagazineBarrelComponent component, ref ComponentGetState args)
|
|
||||||
{
|
|
||||||
(int, int)? count = null;
|
|
||||||
if (component.MagazineContainer.ContainedEntity is {Valid: true} magazine &&
|
|
||||||
TryComp(magazine, out RangedMagazineComponent? rangedMagazineComponent))
|
|
||||||
{
|
|
||||||
count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.State = new MagazineBarrelComponentState(
|
|
||||||
component.ChamberContainer.ContainedEntity != null,
|
|
||||||
component.FireRateSelector,
|
|
||||||
count,
|
|
||||||
component.SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMagazineInteractUsing(EntityUid uid, MagazineBarrelComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (CanInsertMagazine(args.User, args.Used, component, false))
|
|
||||||
{
|
|
||||||
InsertMagazine(args.User, args.Used, component);
|
|
||||||
args.Handled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert 1 ammo
|
|
||||||
if (TryComp(args.Used, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
if (!component.BoltOpen)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-bolt-closed"), component.Owner, Filter.Entities(args.User));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != component.Caliber)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(args.User));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.ChamberContainer.ContainedEntity == null)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-success"), component.Owner, Filter.Entities(args.User));
|
|
||||||
component.ChamberContainer.Insert(args.Used);
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateMagazineAppearance(component);
|
|
||||||
args.Handled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-full"), component.Owner, Filter.Entities(args.User));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMagazineInit(EntityUid uid, MagazineBarrelComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
component.ChamberContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber");
|
|
||||||
component.MagazineContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-magazine", out var existing);
|
|
||||||
|
|
||||||
if (!existing && component.MagFillPrototype != null)
|
|
||||||
{
|
|
||||||
var magEntity = EntityManager.SpawnEntity(component.MagFillPrototype, Transform(uid).Coordinates);
|
|
||||||
component.MagazineContainer.Insert(magEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary coz client doesn't know about magfill.
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMagazineMapInit(EntityUid uid, MagazineBarrelComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
UpdateMagazineAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryEjectChamber(MagazineBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberEntity)
|
|
||||||
{
|
|
||||||
if (!component.ChamberContainer.Remove(chamberEntity))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(chamberEntity);
|
|
||||||
if (!ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
EjectCasing(chamberEntity);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryFeedChamber(MagazineBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (component.ChamberContainer.ContainedEntity != null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try and pull a round from the magazine to replace the chamber if possible
|
|
||||||
var magazine = component.MagazineContainer.ContainedEntity;
|
|
||||||
var magComp = EntityManager.GetComponentOrNull<RangedMagazineComponent>(magazine);
|
|
||||||
|
|
||||||
if (magComp == null || TakeAmmo(magComp) is not {Valid: true} nextRound)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
component.ChamberContainer.Insert(nextRound);
|
|
||||||
|
|
||||||
if (component.AutoEjectMag && magazine != null && EntityManager.GetComponent<RangedMagazineComponent>(magazine.Value).ShotsLeft == 0)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundAutoEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
|
|
||||||
component.MagazineContainer.Remove(magazine.Value);
|
|
||||||
// TODO: Should be a state or something, waste of bandwidth
|
|
||||||
RaiseNetworkEvent(new MagazineAutoEjectEvent {Uid = component.Owner});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CycleMagazine(MagazineBarrelComponent component, bool manual = false)
|
|
||||||
{
|
|
||||||
if (component.BoltOpen)
|
|
||||||
return;
|
|
||||||
|
|
||||||
TryEjectChamber(component);
|
|
||||||
|
|
||||||
TryFeedChamber(component);
|
|
||||||
|
|
||||||
if (component.ChamberContainer.ContainedEntity == null && !component.BoltOpen)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltOpen.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
|
|
||||||
|
|
||||||
if (_container.TryGetContainingContainer(component.Owner, out var container))
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-cycle-bolt-open"), component.Owner, Filter.Entities(container.Owner));
|
|
||||||
}
|
|
||||||
|
|
||||||
component.BoltOpen = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manual)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundRack.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateMagazineAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? PeekAmmo(MagazineBarrelComponent component)
|
|
||||||
{
|
|
||||||
return component.BoltOpen ? null : component.ChamberContainer.ContainedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeProjectile(MagazineBarrelComponent component, EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (component.BoltOpen)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var entity = component.ChamberContainer.ContainedEntity;
|
|
||||||
|
|
||||||
CycleMagazine(component);
|
|
||||||
|
|
||||||
return entity != null ? TakeBullet(EntityManager.GetComponent<AmmoComponent>(entity.Value), spawnAt) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MagazineType> GetMagazineTypes(MagazineBarrelComponent component)
|
|
||||||
{
|
|
||||||
var types = new List<MagazineType>();
|
|
||||||
|
|
||||||
foreach (MagazineType mag in Enum.GetValues(typeof(MagazineType)))
|
|
||||||
{
|
|
||||||
if ((component.MagazineTypes & mag) != 0)
|
|
||||||
{
|
|
||||||
types.Add(mag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return types;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveMagazine(EntityUid user, MagazineBarrelComponent component)
|
|
||||||
{
|
|
||||||
var mag = component.MagazineContainer.ContainedEntity;
|
|
||||||
|
|
||||||
if (mag == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (component.MagNeedsOpenBolt && !component.BoltOpen)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-remove-magazine-bolt-closed"), component.Owner, Filter.Entities(user));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
component.MagazineContainer.Remove(mag.Value);
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
|
|
||||||
_handsSystem.PickupOrDrop(user, mag.Value);
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateMagazineAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanInsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component, bool quiet = true)
|
|
||||||
{
|
|
||||||
if (!TryComp(magazine, out RangedMagazineComponent? magazineComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((component.MagazineTypes & magazineComponent.MagazineType) == 0)
|
|
||||||
{
|
|
||||||
if (!quiet)
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-magazine-type"), component.Owner, Filter.Entities(user));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (magazineComponent.Caliber != component.Caliber)
|
|
||||||
{
|
|
||||||
if (!quiet)
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(user));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.MagNeedsOpenBolt && !component.BoltOpen)
|
|
||||||
{
|
|
||||||
if (!quiet)
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-bolt-closed"), component.Owner, Filter.Entities(user));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.MagazineContainer.ContainedEntity == null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!quiet)
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-already-holding-magazine"), component.Owner, Filter.Entities(user));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-success"), component.Owner, Filter.Entities(user));
|
|
||||||
component.MagazineContainer.Insert(magazine);
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateMagazineAppearance(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void OnPumpExamine(EntityUid uid, PumpBarrelComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
args.PushMarkup(Loc.GetString("pump-barrel-component-on-examine", ("caliber", component.Caliber)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPumpGetState(EntityUid uid, PumpBarrelComponent component, ref ComponentGetState args)
|
|
||||||
{
|
|
||||||
(int, int)? count = (component.ShotsLeft, component.Capacity);
|
|
||||||
var chamberedExists = component.ChamberContainer.ContainedEntity != null;
|
|
||||||
// (Is one chambered?, is the bullet spend)
|
|
||||||
var chamber = (chamberedExists, false);
|
|
||||||
|
|
||||||
if (chamberedExists && TryComp<AmmoComponent?>(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
|
|
||||||
{
|
|
||||||
chamber.Item2 = ammo.Spent;
|
|
||||||
}
|
|
||||||
|
|
||||||
args.State = new PumpBarrelComponentState(
|
|
||||||
chamber,
|
|
||||||
component.FireRateSelector,
|
|
||||||
count,
|
|
||||||
component.SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPumpMapInit(EntityUid uid, PumpBarrelComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
if (component.FillPrototype != null)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount += component.Capacity - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdatePumpAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePumpAppearance(PumpBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
|
||||||
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPumpInit(EntityUid uid, PumpBarrelComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
component.AmmoContainer =
|
|
||||||
uid.EnsureContainer<Container>($"{component.GetType()}-ammo-container", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
component.SpawnedAmmo.Push(entity);
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component.ChamberContainer =
|
|
||||||
uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber-container", out existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryComp(uid, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdatePumpAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPumpUse(EntityUid uid, PumpBarrelComponent component, UseInHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
CyclePump(component, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPumpInteractUsing(EntityUid uid, PumpBarrelComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (TryInsertBullet(component, args))
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertBullet(PumpBarrelComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (!TryComp(args.Used, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != component.Caliber)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(args.User));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
|
|
||||||
{
|
|
||||||
component.AmmoContainer.Insert(args.Used);
|
|
||||||
component.SpawnedAmmo.Push(args.Used);
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdatePumpAppearance(component);
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(args.User));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CyclePump(PumpBarrelComponent component, bool manual = false)
|
|
||||||
{
|
|
||||||
if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberedEntity)
|
|
||||||
{
|
|
||||||
component.ChamberContainer.Remove(chamberedEntity);
|
|
||||||
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(chamberedEntity);
|
|
||||||
if (!ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
EjectCasing(chamberedEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.SpawnedAmmo.TryPop(out var next))
|
|
||||||
{
|
|
||||||
component.AmmoContainer.Remove(next);
|
|
||||||
component.ChamberContainer.Insert(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.UnspawnedCount > 0)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
|
|
||||||
component.ChamberContainer.Insert(ammoEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manual)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
}
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdatePumpAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? PeekAmmo(PumpBarrelComponent component)
|
|
||||||
{
|
|
||||||
return component.ChamberContainer.ContainedEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeProjectile(PumpBarrelComponent component, EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
if (!component.ManualCycle)
|
|
||||||
{
|
|
||||||
CyclePump(component);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
|
|
||||||
|
|
||||||
var ammoComponent = EntityManager.GetComponentOrNull<AmmoComponent>(chamberEntity);
|
|
||||||
|
|
||||||
return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void OnRangedMagMapInit(EntityUid uid, RangedMagazineComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
if (component.FillPrototype != null)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount += component.Capacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateRangedMagAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRangedMagInit(EntityUid uid, RangedMagazineComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-magazine", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
if (component.AmmoContainer.ContainedEntities.Count > component.Capacity)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Initialized capacity of magazine higher than its actual capacity");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
component.SpawnedAmmo.Push(entity);
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
|
|
||||||
{
|
|
||||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateRangedMagAppearance(RangedMagazineComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
|
||||||
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRangedMagUse(EntityUid uid, RangedMagazineComponent component, UseInHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (!TryComp(args.User, out HandsComponent? handsComponent))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TakeAmmo(component) is not {Valid: true} ammo)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_handsSystem.PickupOrDrop(args.User, ammo, handsComp: handsComponent);
|
|
||||||
EjectCasing(ammo);
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRangedMagExamine(EntityUid uid, RangedMagazineComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
args.PushMarkup(Loc.GetString("ranged-magazine-component-on-examine", ("magazineType", component.MagazineType),("caliber", component.Caliber)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRangedMagInteractUsing(EntityUid uid, RangedMagazineComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (TryInsertAmmo(args.User, args.Used, component))
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertAmmo(EntityUid user, EntityUid ammo, RangedMagazineComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(ammo, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != component.Caliber)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.ShotsLeft >= component.Capacity)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-is-full "), component.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
component.AmmoContainer.Insert(ammo);
|
|
||||||
component.SpawnedAmmo.Push(ammo);
|
|
||||||
UpdateRangedMagAppearance(component);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeAmmo(RangedMagazineComponent component)
|
|
||||||
{
|
|
||||||
EntityUid? ammo = null;
|
|
||||||
// If anything's spawned use that first, otherwise use the fill prototype as a fallback (if we have spawn count left)
|
|
||||||
if (component.SpawnedAmmo.TryPop(out var entity))
|
|
||||||
{
|
|
||||||
ammo = entity;
|
|
||||||
component.AmmoContainer.Remove(entity);
|
|
||||||
}
|
|
||||||
else if (component.UnspawnedCount > 0)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
ammo = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateRangedMagAppearance(component);
|
|
||||||
return ammo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void OnRevolverUse(EntityUid uid, RevolverBarrelComponent component, UseInHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
EjectAllSlots(component);
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateRevolverAppearance(component);
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRevolverInteractUsing(EntityUid uid, RevolverBarrelComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (TryInsertBullet(args.User, args.Used, component))
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertBullet(EntityUid user, EntityUid entity, RevolverBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(entity, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != component.Caliber)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Functions like a stack
|
|
||||||
// These are inserted in reverse order but then when fired Cycle will go through in order
|
|
||||||
// The reason we don't just use an actual stack is because spin can select a random slot to point at
|
|
||||||
for (var i = component.AmmoSlots.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var slot = component.AmmoSlots[i];
|
|
||||||
if (slot == default)
|
|
||||||
{
|
|
||||||
component.CurrentSlot = i;
|
|
||||||
component.AmmoSlots[i] = entity;
|
|
||||||
component.AmmoContainer.Insert(entity);
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateRevolverAppearance(component);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-ammo-full"), ammoComponent.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Russian Roulette
|
|
||||||
/// </summary>
|
|
||||||
public void SpinRevolver(RevolverBarrelComponent component)
|
|
||||||
{
|
|
||||||
var random = _random.Next(component.AmmoSlots.Length - 1);
|
|
||||||
component.CurrentSlot = random;
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundSpin.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CycleRevolver(RevolverBarrelComponent component)
|
|
||||||
{
|
|
||||||
// Move up a slot
|
|
||||||
component.CurrentSlot = (component.CurrentSlot + 1) % component.AmmoSlots.Length;
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
UpdateRevolverAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EjectAllSlots(RevolverBarrelComponent component)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < component.AmmoSlots.Length; i++)
|
|
||||||
{
|
|
||||||
var entity = component.AmmoSlots[i];
|
|
||||||
if (entity == null) continue;
|
|
||||||
|
|
||||||
component.AmmoContainer.Remove(entity.Value);
|
|
||||||
EjectCasing(entity.Value);
|
|
||||||
component.AmmoSlots[i] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.AmmoContainer.ContainedEntities.Count > 0)
|
|
||||||
{
|
|
||||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// May as well point back at the end?
|
|
||||||
component.CurrentSlot = component.AmmoSlots.Length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRevolverGetState(EntityUid uid, RevolverBarrelComponent component, ref ComponentGetState args)
|
|
||||||
{
|
|
||||||
var slotsSpent = new bool?[component.Capacity];
|
|
||||||
for (var i = 0; i < component.Capacity; i++)
|
|
||||||
{
|
|
||||||
slotsSpent[i] = null;
|
|
||||||
var ammoEntity = component.AmmoSlots[i];
|
|
||||||
if (ammoEntity != default && TryComp(ammoEntity, out AmmoComponent? ammo))
|
|
||||||
{
|
|
||||||
slotsSpent[i] = ammo.Spent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: make yaml var to not sent currentSlot/UI? (for russian roulette)
|
|
||||||
args.State = new RevolverBarrelComponentState(
|
|
||||||
component.CurrentSlot,
|
|
||||||
component.FireRateSelector,
|
|
||||||
slotsSpent,
|
|
||||||
component.SoundGunshot.GetSound());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRevolverMapInit(EntityUid uid, RevolverBarrelComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount = component.Capacity;
|
|
||||||
var idx = 0;
|
|
||||||
component.AmmoContainer = component.Owner.EnsureContainer<Container>($"{component.GetType()}-ammoContainer", out var existing);
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var entity in component.AmmoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
component.AmmoSlots[idx] = entity;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Revolvers should also defer spawning T B H
|
|
||||||
var xform = EntityManager.GetComponent<TransformComponent>(uid);
|
|
||||||
|
|
||||||
for (var i = 0; i < component.UnspawnedCount; i++)
|
|
||||||
{
|
|
||||||
var entity = EntityManager.SpawnEntity(component.FillPrototype, xform.Coordinates);
|
|
||||||
component.AmmoSlots[idx] = entity;
|
|
||||||
component.AmmoContainer.Insert(entity);
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateRevolverAppearance(component);
|
|
||||||
component.Dirty(EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateRevolverAppearance(RevolverBarrelComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(component.Owner, out AppearanceComponent? appearance))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder, at this stage it's just here for the RPG
|
|
||||||
appearance.SetData(MagazineBarrelVisuals.MagLoaded, component.ShotsLeft > 0);
|
|
||||||
appearance.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
|
|
||||||
appearance.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddSpinVerb(EntityUid uid, RevolverBarrelComponent component, GetVerbsEvent<AlternativeVerb> args)
|
|
||||||
{
|
|
||||||
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (component.Capacity <= 1 || component.ShotsLeft == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AlternativeVerb verb = new()
|
|
||||||
{
|
|
||||||
Text = Loc.GetString("spin-revolver-verb-get-data-text"),
|
|
||||||
IconTexture = "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png",
|
|
||||||
Act = () =>
|
|
||||||
{
|
|
||||||
SpinRevolver(component);
|
|
||||||
component.Owner.PopupMessage(args.User, Loc.GetString("spin-revolver-verb-on-activate"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
args.Verbs.Add(verb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? PeekAmmo(RevolverBarrelComponent component)
|
|
||||||
{
|
|
||||||
return component.AmmoSlots[component.CurrentSlot];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Takes a projectile out if possible
|
|
||||||
/// IEnumerable just to make supporting shotguns saner
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="NotImplementedException"></exception>
|
|
||||||
public EntityUid? TakeProjectile(RevolverBarrelComponent component, EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
var ammo = component.AmmoSlots[component.CurrentSlot];
|
|
||||||
EntityUid? bullet = null;
|
|
||||||
if (ammo != null)
|
|
||||||
{
|
|
||||||
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(ammo.Value);
|
|
||||||
bullet = TakeBullet(ammoComponent, spawnAt);
|
|
||||||
if (ammoComponent.Caseless)
|
|
||||||
{
|
|
||||||
component.AmmoSlots[component.CurrentSlot] = null;
|
|
||||||
component.AmmoContainer.Remove(ammo.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CycleRevolver(component);
|
|
||||||
UpdateRevolverAppearance(component);
|
|
||||||
return bullet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Weapons.Ranged.Barrels.Components;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem
|
|
||||||
{
|
|
||||||
private void OnSpeedLoaderInit(EntityUid uid, SpeedLoaderComponent component, ComponentInit args)
|
|
||||||
{
|
|
||||||
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-container", out var existing);
|
|
||||||
|
|
||||||
if (existing)
|
|
||||||
{
|
|
||||||
foreach (var ammo in component.AmmoContainer.ContainedEntities)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
component.SpawnedAmmo.Push(ammo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSpeedLoaderMapInit(EntityUid uid, SpeedLoaderComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount += component.Capacity;
|
|
||||||
UpdateSpeedLoaderAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateSpeedLoaderAppearance(SpeedLoaderComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
|
|
||||||
|
|
||||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.AmmoLeft);
|
|
||||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EntityUid? TakeAmmo(SpeedLoaderComponent component)
|
|
||||||
{
|
|
||||||
if (component.SpawnedAmmo.TryPop(out var entity))
|
|
||||||
{
|
|
||||||
component.AmmoContainer.Remove(entity);
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.UnspawnedCount > 0)
|
|
||||||
{
|
|
||||||
component.UnspawnedCount--;
|
|
||||||
return EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSpeedLoaderUse(EntityUid uid, SpeedLoaderComponent component, UseInHandEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (!TryComp(args.User, out HandsComponent? handsComponent))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ammo = TakeAmmo(component);
|
|
||||||
if (ammo == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_handsSystem.TryPickup(args.User, ammo.Value, handsComp: handsComponent))
|
|
||||||
{
|
|
||||||
EjectCasing(ammo.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateSpeedLoaderAppearance(component);
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSpeedLoaderAfterInteract(EntityUid uid, SpeedLoaderComponent component, AfterInteractEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled || !args.CanReach) return;
|
|
||||||
|
|
||||||
if (args.Target == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
|
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
if (TryComp(args.Target.Value, out RevolverBarrelComponent? revolverBarrel))
|
|
||||||
{
|
|
||||||
for (var i = 0; i < component.Capacity; i++)
|
|
||||||
{
|
|
||||||
var ammo = TakeAmmo(component);
|
|
||||||
if (ammo == null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryInsertBullet(args.User, ammo.Value, revolverBarrel))
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take the ammo back
|
|
||||||
TryInsertAmmo(args.User, ammo.Value, component);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (TryComp(args.Target.Value, out BoltActionBarrelComponent? boltActionBarrel))
|
|
||||||
{
|
|
||||||
for (var i = 0; i < component.Capacity; i++)
|
|
||||||
{
|
|
||||||
var ammo = TakeAmmo(component);
|
|
||||||
if (ammo == null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryInsertBullet(args.User, ammo.Value, boltActionBarrel))
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take the ammo back
|
|
||||||
TryInsertAmmo(args.User, ammo.Value, component);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
UpdateSpeedLoaderAppearance(component);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryInsertAmmo(EntityUid user, EntityUid entity, SpeedLoaderComponent component)
|
|
||||||
{
|
|
||||||
if (!TryComp(entity, out AmmoComponent? ammoComponent))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ammoComponent.Caliber != component.Caliber)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.AmmoLeft >= component.Capacity)
|
|
||||||
{
|
|
||||||
_popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-no-room"), component.Owner, Filter.Entities(user));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
component.SpawnedAmmo.Push(entity);
|
|
||||||
component.AmmoContainer.Insert(entity);
|
|
||||||
UpdateSpeedLoaderAppearance(component);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSpeedLoaderInteractUsing(EntityUid uid, SpeedLoaderComponent component, InteractUsingEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled) return;
|
|
||||||
|
|
||||||
if (TryInsertAmmo(args.User, args.Used, component))
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
using Content.Server.Administration.Logs;
|
|
||||||
using Content.Server.Atmos.EntitySystems;
|
|
||||||
using Content.Server.Hands.Components;
|
|
||||||
using Content.Server.PowerCell;
|
|
||||||
using Content.Server.Stunnable;
|
|
||||||
using Content.Server.Weapon.Melee;
|
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Camera;
|
|
||||||
using Content.Shared.Damage;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.Hands.EntitySystems;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.PowerCell.Components;
|
|
||||||
using Content.Shared.Verbs;
|
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Random;
|
|
||||||
using Robust.Shared.Timing;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
|
||||||
|
|
||||||
public sealed partial class GunSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
||||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
||||||
[Dependency] private readonly AtmosphereSystem _atmos = default!;
|
|
||||||
[Dependency] private readonly CameraRecoilSystem _recoil = default!;
|
|
||||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
|
||||||
[Dependency] private readonly EffectSystem _effects = default!;
|
|
||||||
[Dependency] private readonly PowerCellSystem _cell = default!;
|
|
||||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
|
||||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
|
||||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
||||||
[Dependency] private readonly StunSystem _stun = default!;
|
|
||||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
|
||||||
|
|
||||||
public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How many sounds are allowed to be played on ejecting multiple casings.
|
|
||||||
/// </summary>
|
|
||||||
private const int EjectionSoundMax = 3;
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
// TODO: So at the time I thought there might've been a need to keep magazines
|
|
||||||
// and ammo boxes separate.
|
|
||||||
// There isn't.
|
|
||||||
// They should be combined.
|
|
||||||
|
|
||||||
SubscribeLocalEvent<AmmoComponent, ExaminedEvent>(OnAmmoExamine);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, ComponentInit>(OnAmmoBoxInit);
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, MapInitEvent>(OnAmmoBoxMapInit);
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, ExaminedEvent>(OnAmmoBoxExamine);
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, InteractUsingEvent>(OnAmmoBoxInteractUsing);
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, UseInHandEvent>(OnAmmoBoxUse);
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, InteractHandEvent>(OnAmmoBoxInteractHand);
|
|
||||||
SubscribeLocalEvent<AmmoBoxComponent, GetVerbsEvent<AlternativeVerb>>(OnAmmoBoxAltVerbs);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<RangedMagazineComponent, ComponentInit>(OnRangedMagInit);
|
|
||||||
SubscribeLocalEvent<RangedMagazineComponent, MapInitEvent>(OnRangedMagMapInit);
|
|
||||||
SubscribeLocalEvent<RangedMagazineComponent, UseInHandEvent>(OnRangedMagUse);
|
|
||||||
SubscribeLocalEvent<RangedMagazineComponent, ExaminedEvent>(OnRangedMagExamine);
|
|
||||||
SubscribeLocalEvent<RangedMagazineComponent, InteractUsingEvent>(OnRangedMagInteractUsing);
|
|
||||||
|
|
||||||
// Whenever I get around to refactoring guns this is all going to change.
|
|
||||||
// Essentially the idea is
|
|
||||||
// You have GunComponent and ChamberedGunComponent (which is just guncomp + containerslot for chamber)
|
|
||||||
// GunComponent has a component for an ammo provider on it (e.g. battery) and asks it for ammo to shoot
|
|
||||||
// ALTERNATIVELY, it has a "MagazineAmmoProvider" that has its own containerslot that it can ask
|
|
||||||
// (All of these would be comp references so max you only ever have 2 components on the gun).
|
|
||||||
SubscribeLocalEvent<BatteryBarrelComponent, ComponentInit>(OnBatteryInit);
|
|
||||||
SubscribeLocalEvent<BatteryBarrelComponent, MapInitEvent>(OnBatteryMapInit);
|
|
||||||
SubscribeLocalEvent<BatteryBarrelComponent, PowerCellChangedEvent>(OnCellSlotUpdated);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, ComponentInit>(OnBoltInit);
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, MapInitEvent>(OnBoltMapInit);
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, GunFireAttemptEvent>(OnBoltFireAttempt);
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, UseInHandEvent>(OnBoltUse);
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, InteractUsingEvent>(OnBoltInteractUsing);
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, ComponentGetState>(OnBoltGetState);
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, ExaminedEvent>(OnBoltExamine);
|
|
||||||
SubscribeLocalEvent<BoltActionBarrelComponent, GetVerbsEvent<InteractionVerb>>(AddToggleBoltVerb);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, ComponentInit>(OnMagazineInit);
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, MapInitEvent>(OnMagazineMapInit);
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, ExaminedEvent>(OnMagazineExamine);
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, UseInHandEvent>(OnMagazineUse);
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, InteractUsingEvent>(OnMagazineInteractUsing);
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, ComponentGetState>(OnMagazineGetState);
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, GetVerbsEvent<InteractionVerb>>(AddMagazineInteractionVerbs);
|
|
||||||
SubscribeLocalEvent<MagazineBarrelComponent, GetVerbsEvent<AlternativeVerb>>(AddEjectMagazineVerb);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<PumpBarrelComponent, ComponentGetState>(OnPumpGetState);
|
|
||||||
SubscribeLocalEvent<PumpBarrelComponent, ComponentInit>(OnPumpInit);
|
|
||||||
SubscribeLocalEvent<PumpBarrelComponent, MapInitEvent>(OnPumpMapInit);
|
|
||||||
SubscribeLocalEvent<PumpBarrelComponent, ExaminedEvent>(OnPumpExamine);
|
|
||||||
SubscribeLocalEvent<PumpBarrelComponent, UseInHandEvent>(OnPumpUse);
|
|
||||||
SubscribeLocalEvent<PumpBarrelComponent, InteractUsingEvent>(OnPumpInteractUsing);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<RevolverBarrelComponent, MapInitEvent>(OnRevolverMapInit);
|
|
||||||
SubscribeLocalEvent<RevolverBarrelComponent, UseInHandEvent>(OnRevolverUse);
|
|
||||||
SubscribeLocalEvent<RevolverBarrelComponent, InteractUsingEvent>(OnRevolverInteractUsing);
|
|
||||||
SubscribeLocalEvent<RevolverBarrelComponent, ComponentGetState>(OnRevolverGetState);
|
|
||||||
SubscribeLocalEvent<RevolverBarrelComponent, GetVerbsEvent<AlternativeVerb>>(AddSpinVerb);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<SpeedLoaderComponent, ComponentInit>(OnSpeedLoaderInit);
|
|
||||||
SubscribeLocalEvent<SpeedLoaderComponent, MapInitEvent>(OnSpeedLoaderMapInit);
|
|
||||||
SubscribeLocalEvent<SpeedLoaderComponent, UseInHandEvent>(OnSpeedLoaderUse);
|
|
||||||
SubscribeLocalEvent<SpeedLoaderComponent, AfterInteractEvent>(OnSpeedLoaderAfterInteract);
|
|
||||||
SubscribeLocalEvent<SpeedLoaderComponent, InteractUsingEvent>(OnSpeedLoaderInteractUsing);
|
|
||||||
|
|
||||||
// SubscribeLocalEvent<ServerRangedWeaponComponent, ExaminedEvent>(OnGunExamine);
|
|
||||||
SubscribeNetworkEvent<FirePosEvent>(OnFirePos);
|
|
||||||
SubscribeLocalEvent<ServerRangedWeaponComponent, MeleeAttackAttemptEvent>(OnMeleeAttempt);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnFirePos(FirePosEvent msg, EntitySessionEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.SenderSession.AttachedEntity is not {Valid: true} user)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!msg.Coordinates.IsValid(EntityManager))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!TryComp(user, out HandsComponent? handsComponent))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO: Not exactly robust
|
|
||||||
var gun = handsComponent.ActiveHand?.HeldEntity;
|
|
||||||
|
|
||||||
if (gun == null || !TryComp(gun, out ServerRangedWeaponComponent? weapon))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// map pos
|
|
||||||
TryFire(user, msg.Coordinates, weapon);
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? PeekAtAmmo(ServerRangedBarrelComponent component)
|
|
||||||
{
|
|
||||||
return component switch
|
|
||||||
{
|
|
||||||
BatteryBarrelComponent battery => PeekAmmo(battery),
|
|
||||||
BoltActionBarrelComponent bolt => PeekAmmo(bolt),
|
|
||||||
MagazineBarrelComponent mag => PeekAmmo(mag),
|
|
||||||
PumpBarrelComponent pump => PeekAmmo(pump),
|
|
||||||
RevolverBarrelComponent revolver => PeekAmmo(revolver),
|
|
||||||
_ => throw new NotImplementedException()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public EntityUid? TakeOutProjectile(ServerRangedBarrelComponent component, EntityCoordinates spawnAt)
|
|
||||||
{
|
|
||||||
return component switch
|
|
||||||
{
|
|
||||||
BatteryBarrelComponent battery => TakeProjectile(battery, spawnAt),
|
|
||||||
BoltActionBarrelComponent bolt => TakeProjectile(bolt, spawnAt),
|
|
||||||
MagazineBarrelComponent mag => TakeProjectile(mag, spawnAt),
|
|
||||||
PumpBarrelComponent pump => TakeProjectile(pump, spawnAt),
|
|
||||||
RevolverBarrelComponent revolver => TakeProjectile(revolver, spawnAt),
|
|
||||||
_ => throw new NotImplementedException()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Drops multiple cartridges / shells on the floor
|
|
||||||
/// Wraps EjectCasing to make it less toxic for bulk ejections
|
|
||||||
/// </summary>
|
|
||||||
public void EjectCasings(IEnumerable<EntityUid> entities)
|
|
||||||
{
|
|
||||||
var soundPlayCount = 0;
|
|
||||||
var playSound = true;
|
|
||||||
|
|
||||||
foreach (var entity in entities)
|
|
||||||
{
|
|
||||||
EjectCasing(entity, playSound);
|
|
||||||
soundPlayCount++;
|
|
||||||
if (soundPlayCount > EjectionSoundMax)
|
|
||||||
{
|
|
||||||
playSound = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Drops a single cartridge / shell
|
|
||||||
/// </summary>
|
|
||||||
public void EjectCasing(
|
|
||||||
EntityUid entity,
|
|
||||||
bool playSound = true,
|
|
||||||
AmmoComponent? ammoComponent = null)
|
|
||||||
{
|
|
||||||
const float ejectOffset = 0.4f;
|
|
||||||
|
|
||||||
if (!Resolve(entity, ref ammoComponent)) return;
|
|
||||||
|
|
||||||
var offsetPos = (_random.NextFloat(-ejectOffset, ejectOffset), _random.NextFloat(-ejectOffset, ejectOffset));
|
|
||||||
|
|
||||||
var xform = Transform(entity);
|
|
||||||
|
|
||||||
var coordinates = xform.Coordinates;
|
|
||||||
coordinates = coordinates.Offset(offsetPos);
|
|
||||||
|
|
||||||
xform.LocalRotation = _random.NextFloat(MathF.Tau);
|
|
||||||
xform.Coordinates = coordinates;
|
|
||||||
|
|
||||||
if (playSound)
|
|
||||||
SoundSystem.Play(Filter.Pvs(entity), ammoComponent.SoundCollectionEject.GetSound(), coordinates, AudioParams.Default.WithVolume(-1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Angle GetRecoilAngle(ServerRangedBarrelComponent component, Angle direction)
|
|
||||||
{
|
|
||||||
var currentTime = _gameTiming.CurTime;
|
|
||||||
var timeSinceLastFire = (currentTime - component.LastFire).TotalSeconds;
|
|
||||||
var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease - component.AngleDecay * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
|
|
||||||
component.CurrentAngle = new Angle(newTheta);
|
|
||||||
|
|
||||||
var random = (_random.NextDouble(-1, 1));
|
|
||||||
var angle = Angle.FromDegrees(direction.Degrees + component.CurrentAngle.Degrees * random);
|
|
||||||
return angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Raised on a gun when it fires.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GunShotEvent : EntityEventArgs
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GunFireAttemptEvent : CancellableEntityEventArgs
|
|
||||||
{
|
|
||||||
public EntityUid? User = null;
|
|
||||||
public ServerRangedWeaponComponent Weapon;
|
|
||||||
|
|
||||||
public GunFireAttemptEvent(EntityUid? user, ServerRangedWeaponComponent weapon)
|
|
||||||
{
|
|
||||||
User = user;
|
|
||||||
Weapon = weapon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using Content.Shared.Hands;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged
|
|
||||||
{
|
|
||||||
public sealed class RangedWeaponSysten : EntitySystem
|
|
||||||
{
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
SubscribeLocalEvent<ServerRangedWeaponComponent, HandSelectedEvent>(OnHandSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnHandSelected(EntityUid uid, ServerRangedWeaponComponent component, HandSelectedEvent args)
|
|
||||||
{
|
|
||||||
// Instead of dirtying on hand-select this component should probably by dirtied whenever it needs to be.
|
|
||||||
// I take no responsibility for this code. It was like this when I got here.
|
|
||||||
|
|
||||||
Dirty(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using Content.Shared.Sound;
|
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged
|
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent
|
|
||||||
{
|
|
||||||
public TimeSpan LastFireTime;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("clumsyCheck")]
|
|
||||||
public bool ClumsyCheck { get; set; } = true;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("clumsyExplodeChance")]
|
|
||||||
public float ClumsyExplodeChance { get; set; } = 0.5f;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("canHotspot")]
|
|
||||||
public bool CanHotspot = true;
|
|
||||||
|
|
||||||
[DataField("clumsyWeaponHandlingSound")]
|
|
||||||
public SoundSpecifier ClumsyWeaponHandlingSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg");
|
|
||||||
|
|
||||||
[DataField("clumsyWeaponShotSound")]
|
|
||||||
public SoundSpecifier ClumsyWeaponShotSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Chemistry.EntitySystems;
|
using Content.Server.Chemistry.EntitySystems;
|
||||||
using Content.Server.Weapon.Ranged.Ammunition.Components;
|
using Content.Server.Weapon.Ranged.Components;
|
||||||
using Content.Server.Weapon.Ranged.Barrels.Components;
|
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Content.Shared.Weapons.Ranged.Events;
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged
|
namespace Content.Server.Weapon.Ranged.Systems
|
||||||
{
|
{
|
||||||
public sealed class ChemicalAmmoSystem : EntitySystem
|
public sealed class ChemicalAmmoSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private SolutionContainerSystem _solutionSystem = default!;
|
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
5
Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs
Normal file
5
Content.Server/Weapon/Ranged/Systems/FlyBySoundSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using Content.Shared.Weapons.Ranged.Systems;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Systems;
|
||||||
|
|
||||||
|
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}
|
||||||
31
Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs
Normal file
31
Content.Server/Weapon/Ranged/Systems/GunSystem.Ballistic.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Systems;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
EjectCartridge(ent.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs
Normal file
59
Content.Server/Weapon/Ranged/Systems/GunSystem.Battery.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using Content.Server.Power.Components;
|
||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Systems;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
protected override void InitializeBattery()
|
||||||
|
{
|
||||||
|
base.InitializeBattery();
|
||||||
|
|
||||||
|
// Hitscan
|
||||||
|
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
|
||||||
|
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
|
||||||
|
|
||||||
|
// Projectile
|
||||||
|
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
|
||||||
|
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBatteryStartup(EntityUid uid, BatteryAmmoProviderComponent component, ComponentStartup args)
|
||||||
|
{
|
||||||
|
UpdateShots(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBatteryChargeChange(EntityUid uid, BatteryAmmoProviderComponent component, ChargeChangedEvent args)
|
||||||
|
{
|
||||||
|
UpdateShots(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp<BatteryComponent>(uid, out var battery)) return;
|
||||||
|
UpdateShots(component, battery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateShots(BatteryAmmoProviderComponent component, BatteryComponent battery)
|
||||||
|
{
|
||||||
|
var shots = (int) (battery.CurrentCharge / component.FireCost);
|
||||||
|
var maxShots = (int) (battery.MaxCharge / component.FireCost);
|
||||||
|
|
||||||
|
if (component.Shots != shots || component.Capacity != maxShots)
|
||||||
|
{
|
||||||
|
Dirty(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Shots = shots;
|
||||||
|
component.Capacity = maxShots;
|
||||||
|
UpdateBatteryAppearance(component.Owner, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component)
|
||||||
|
{
|
||||||
|
if (!TryComp<BatteryComponent>(uid, out var battery)) return;
|
||||||
|
|
||||||
|
battery.CurrentCharge -= component.FireCost;
|
||||||
|
UpdateShots(component, battery);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs
Normal file
17
Content.Server/Weapon/Ranged/Systems/GunSystem.Revolver.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Systems;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem
|
||||||
|
{
|
||||||
|
protected override void SpinRevolver(RevolverAmmoProviderComponent component, EntityUid? user = null)
|
||||||
|
{
|
||||||
|
base.SpinRevolver(component, user);
|
||||||
|
var index = Random.Next(component.Capacity);
|
||||||
|
|
||||||
|
if (component.CurrentIndex == index) return;
|
||||||
|
|
||||||
|
component.CurrentIndex = index;
|
||||||
|
Dirty(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
298
Content.Server/Weapon/Ranged/Systems/GunSystem.cs
Normal file
298
Content.Server/Weapon/Ranged/Systems/GunSystem.cs
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Content.Server.Projectiles.Components;
|
||||||
|
using Content.Server.Weapon.Melee;
|
||||||
|
using Content.Server.Weapon.Ranged.Components;
|
||||||
|
using Content.Shared.Audio;
|
||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.Sound;
|
||||||
|
using Content.Shared.Throwing;
|
||||||
|
using Content.Shared.Weapons.Ranged;
|
||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
using Content.Shared.Weapons.Ranged.Events;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Physics;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
|
||||||
|
|
||||||
|
namespace Content.Server.Weapon.Ranged.Systems;
|
||||||
|
|
||||||
|
public sealed partial class GunSystem : SharedGunSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly EffectSystem _effects = default!;
|
||||||
|
|
||||||
|
public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
|
||||||
|
|
||||||
|
public override void Shoot(GunComponent gun, List<IShootable> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null)
|
||||||
|
{
|
||||||
|
var fromMap = fromCoordinates.ToMap(EntityManager);
|
||||||
|
var toMap = toCoordinates.ToMapPos(EntityManager);
|
||||||
|
var mapDirection = toMap - fromMap.Position;
|
||||||
|
var mapAngle = mapDirection.ToAngle();
|
||||||
|
var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle());
|
||||||
|
|
||||||
|
// Update shot based on the recoil
|
||||||
|
toMap = fromMap.Position + angle.ToVec() * mapDirection.Length;
|
||||||
|
mapDirection = toMap - fromMap.Position;
|
||||||
|
var entityDirection = Transform(fromCoordinates.EntityId).InvWorldMatrix.Transform(toMap) - fromCoordinates.Position;
|
||||||
|
|
||||||
|
// I must be high because this was getting tripped even when true.
|
||||||
|
// DebugTools.Assert(direction != Vector2.Zero);
|
||||||
|
var shotProjectiles = new List<EntityUid>(ammo.Count);
|
||||||
|
|
||||||
|
foreach (var shootable in ammo)
|
||||||
|
{
|
||||||
|
switch (shootable)
|
||||||
|
{
|
||||||
|
// Cartridge shoots something else
|
||||||
|
case CartridgeAmmoComponent cartridge:
|
||||||
|
if (!cartridge.Spent)
|
||||||
|
{
|
||||||
|
if (cartridge.Count > 1)
|
||||||
|
{
|
||||||
|
var angles = LinearSpread(mapAngle - Angle.FromDegrees(cartridge.Spread / 2f),
|
||||||
|
mapAngle + Angle.FromDegrees(cartridge.Spread / 2f), cartridge.Count);
|
||||||
|
|
||||||
|
for (var i = 0; i < cartridge.Count; i++)
|
||||||
|
{
|
||||||
|
var uid = Spawn(cartridge.Prototype, fromCoordinates);
|
||||||
|
ShootProjectile(uid, angles[i].ToVec(), user);
|
||||||
|
shotProjectiles.Add(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var uid = Spawn(cartridge.Prototype, fromCoordinates);
|
||||||
|
ShootProjectile(uid, mapDirection, user);
|
||||||
|
shotProjectiles.Add(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetCartridgeSpent(cartridge, true);
|
||||||
|
MuzzleFlash(gun.Owner, cartridge, user);
|
||||||
|
|
||||||
|
if (cartridge.DeleteOnSpawn)
|
||||||
|
Del(cartridge.Owner);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlaySound(gun.Owner, gun.SoundEmpty?.GetSound(Random, ProtoManager), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something like ballistic might want to leave it in the container still
|
||||||
|
if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(cartridge.Owner))
|
||||||
|
EjectCartridge(cartridge.Owner);
|
||||||
|
|
||||||
|
Dirty(cartridge);
|
||||||
|
break;
|
||||||
|
// Ammo shoots itself
|
||||||
|
case AmmoComponent newAmmo:
|
||||||
|
shotProjectiles.Add(newAmmo.Owner);
|
||||||
|
MuzzleFlash(gun.Owner, newAmmo, user);
|
||||||
|
|
||||||
|
// Do a throw
|
||||||
|
if (!HasComp<ProjectileComponent>(newAmmo.Owner))
|
||||||
|
{
|
||||||
|
RemComp<AmmoComponent>(newAmmo.Owner);
|
||||||
|
// TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
|
||||||
|
ThrowingSystem.TryThrow(newAmmo.Owner, mapDirection, 20f, user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShootProjectile(newAmmo.Owner, mapDirection, user);
|
||||||
|
break;
|
||||||
|
case HitscanPrototype hitscan:
|
||||||
|
var ray = new CollisionRay(fromMap.Position, mapDirection.Normalized, hitscan.CollisionMask);
|
||||||
|
var rayCastResults = Physics.IntersectRay(fromMap.MapId, ray, hitscan.MaxLength, user, false).ToList();
|
||||||
|
|
||||||
|
if (rayCastResults.Count >= 1)
|
||||||
|
{
|
||||||
|
var result = rayCastResults[0];
|
||||||
|
var distance = result.Distance;
|
||||||
|
FireEffects(fromCoordinates, distance, entityDirection.ToAngle(), hitscan, result.HitEntity);
|
||||||
|
|
||||||
|
var dmg = hitscan.Damage;
|
||||||
|
|
||||||
|
if (dmg != null)
|
||||||
|
dmg = Damageable.TryChangeDamage(result.HitEntity, dmg);
|
||||||
|
|
||||||
|
if (dmg != null)
|
||||||
|
{
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
Logs.Add(LogType.HitScanHit,
|
||||||
|
$"{ToPrettyString(user.Value):user} hit {ToPrettyString(result.HitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logs.Add(LogType.HitScanHit,
|
||||||
|
$"Hit {ToPrettyString(result.HitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FireEffects(fromCoordinates, hitscan.MaxLength, entityDirection.ToAngle(), hitscan);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RaiseLocalEvent(gun.Owner, new AmmoShotEvent()
|
||||||
|
{
|
||||||
|
FiredProjectiles = shotProjectiles,
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShootProjectile(EntityUid uid, Vector2 direction, EntityUid? user = null)
|
||||||
|
{
|
||||||
|
var physics = EnsureComp<PhysicsComponent>(uid);
|
||||||
|
physics.BodyStatus = BodyStatus.InAir;
|
||||||
|
physics.LinearVelocity = direction.Normalized * 20f;
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
var projectile = EnsureComp<ProjectileComponent>(uid);
|
||||||
|
projectile.IgnoreEntity(user.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform(uid).WorldRotation = direction.ToWorldAngle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a linear spread of angles between start and end.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="start">Start angle in degrees</param>
|
||||||
|
/// <param name="end">End angle in degrees</param>
|
||||||
|
/// <param name="intervals">How many shots there are</param>
|
||||||
|
private Angle[] LinearSpread(Angle start, Angle end, int intervals)
|
||||||
|
{
|
||||||
|
var angles = new Angle[intervals];
|
||||||
|
DebugTools.Assert(intervals > 1);
|
||||||
|
|
||||||
|
for (var i = 0; i <= intervals - 1; i++)
|
||||||
|
{
|
||||||
|
angles[i] = new Angle(start + (end - start) * i / (intervals - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return angles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction)
|
||||||
|
{
|
||||||
|
var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds;
|
||||||
|
var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease.Theta - component.AngleDecay.Theta * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
|
||||||
|
component.CurrentAngle = new Angle(newTheta);
|
||||||
|
component.LastFire = component.NextFire;
|
||||||
|
|
||||||
|
// Convert it so angle can go either side.
|
||||||
|
var random = Random.NextGaussian(0, 0.5);
|
||||||
|
var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random);
|
||||||
|
return angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PlaySound(EntityUid gun, string? sound, EntityUid? user = null)
|
||||||
|
{
|
||||||
|
if (sound == null) return;
|
||||||
|
|
||||||
|
SoundSystem.Play(Filter.Pvs(gun, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user), sound, gun);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Popup(string message, EntityUid? uid, EntityUid? user) {}
|
||||||
|
|
||||||
|
protected override void CreateEffect(EffectSystemMessage message, EntityUid? user = null)
|
||||||
|
{
|
||||||
|
// TODO: Fucking bad
|
||||||
|
if (TryComp<ActorComponent>(user, out var actor))
|
||||||
|
{
|
||||||
|
_effects.CreateParticle(message, actor.PlayerSession);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_effects.CreateParticle(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
|
||||||
|
{
|
||||||
|
// Like projectiles and melee,
|
||||||
|
// 1. Entity specific sound
|
||||||
|
// 2. Ammo's sound
|
||||||
|
// 3. Nothing
|
||||||
|
var playedSound = false;
|
||||||
|
|
||||||
|
if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.Total > 0 && TryComp<RangedDamageSoundComponent>(otherEntity, out var rangedSound))
|
||||||
|
{
|
||||||
|
var type = MeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, ProtoManager);
|
||||||
|
|
||||||
|
if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(
|
||||||
|
Filter.Pvs(otherEntity, entityManager: EntityManager),
|
||||||
|
damageSoundType!.GetSound(),
|
||||||
|
otherEntity,
|
||||||
|
AudioHelpers.WithVariation(DamagePitchVariation));
|
||||||
|
|
||||||
|
playedSound = true;
|
||||||
|
}
|
||||||
|
else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
|
||||||
|
{
|
||||||
|
SoundSystem.Play(
|
||||||
|
Filter.Pvs(otherEntity, entityManager: EntityManager),
|
||||||
|
damageSoundGroup!.GetSound(),
|
||||||
|
otherEntity,
|
||||||
|
AudioHelpers.WithVariation(DamagePitchVariation));
|
||||||
|
|
||||||
|
playedSound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playedSound && weaponSound != null)
|
||||||
|
SoundSystem.Play(Filter.Pvs(otherEntity, entityManager: EntityManager), weaponSound.GetSound(), otherEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pseudo RNG so the client can predict these.
|
||||||
|
#region Hitscan effects
|
||||||
|
|
||||||
|
private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle angle, HitscanPrototype hitscan, EntityUid? hitEntity = null)
|
||||||
|
{
|
||||||
|
// Lord
|
||||||
|
// Forgive me for the shitcode I am about to do
|
||||||
|
// Effects tempt me not
|
||||||
|
var sprites = new List<(EntityCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
|
||||||
|
|
||||||
|
// We'll get the effects relative to the grid / map of the firer
|
||||||
|
if (distance >= 1f)
|
||||||
|
{
|
||||||
|
if (hitscan.MuzzleFlash != null)
|
||||||
|
{
|
||||||
|
sprites.Add((fromCoordinates.Offset(angle.ToVec().Normalized / 2), angle, hitscan.MuzzleFlash, 1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitscan.TravelFlash != null)
|
||||||
|
{
|
||||||
|
sprites.Add((fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2), angle, hitscan.TravelFlash, distance - 1.5f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitscan.ImpactFlash != null)
|
||||||
|
{
|
||||||
|
sprites.Add((fromCoordinates.Offset(angle.ToVec() * distance), angle.FlipPositive(), hitscan.ImpactFlash, 1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sprites.Count > 0)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(new HitscanEvent()
|
||||||
|
{
|
||||||
|
Sprites = sprites,
|
||||||
|
}, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Content.Server.Ghost.Components;
|
using Content.Server.Ghost.Components;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
using Content.Shared.Weapons.Ranged;
|
using Content.Shared.Weapons.Ranged.Systems;
|
||||||
using Robust.Server.Console;
|
using Robust.Server.Console;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
@@ -11,7 +11,7 @@ using Robust.Shared.Players;
|
|||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Weapon.Ranged;
|
namespace Content.Server.Weapon.Ranged.Systems;
|
||||||
|
|
||||||
public sealed class TetherGunSystem : SharedTetherGunSystem
|
public sealed class TetherGunSystem : SharedTetherGunSystem
|
||||||
{
|
{
|
||||||
@@ -52,6 +52,11 @@ namespace Content.Shared.CombatMode
|
|||||||
_actionsSystem.RemoveAction(uid, component.DisarmAction);
|
_actionsSystem.RemoveAction(uid, component.DisarmAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsInCombatMode(EntityUid entity)
|
||||||
|
{
|
||||||
|
return TryComp<SharedCombatModeComponent>(entity, out var combatMode) && combatMode.IsInCombatMode;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args)
|
private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args)
|
||||||
{
|
{
|
||||||
if (args.Handled)
|
if (args.Handled)
|
||||||
|
|||||||
@@ -203,6 +203,8 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
// ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage
|
// ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage
|
||||||
|
|
||||||
PlaySound(uid, slot.InsertSound, slot.SoundOptions, excludeUserAudio ? user : null);
|
PlaySound(uid, slot.InsertSound, slot.SoundOptions, excludeUserAudio ? user : null);
|
||||||
|
var ev = new ItemSlotChangedEvent();
|
||||||
|
RaiseLocalEvent(uid, ref ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -326,6 +328,8 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
// ContainerSlot automatically raises a directed EntRemovedFromContainerMessage
|
// ContainerSlot automatically raises a directed EntRemovedFromContainerMessage
|
||||||
|
|
||||||
PlaySound(uid, slot.EjectSound, slot.SoundOptions, excludeUserAudio ? user : null);
|
PlaySound(uid, slot.EjectSound, slot.SoundOptions, excludeUserAudio ? user : null);
|
||||||
|
var ev = new ItemSlotChangedEvent();
|
||||||
|
RaiseLocalEvent(uid, ref ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -336,13 +340,13 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
{
|
{
|
||||||
item = null;
|
item = null;
|
||||||
|
|
||||||
/// This handles logic with the slot itself
|
// This handles logic with the slot itself
|
||||||
if (!CanEject(slot))
|
if (!CanEject(slot))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
item = slot.Item;
|
item = slot.Item;
|
||||||
|
|
||||||
/// This handles user logic
|
// This handles user logic
|
||||||
if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value))
|
if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -354,7 +358,7 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
/// Try to eject item from a slot.
|
/// Try to eject item from a slot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>False if the id is not valid, the item slot is locked, or it has no item inserted</returns>
|
/// <returns>False if the id is not valid, the item slot is locked, or it has no item inserted</returns>
|
||||||
public bool TryEject(EntityUid uid, string id, EntityUid user,
|
public bool TryEject(EntityUid uid, string id, EntityUid? user,
|
||||||
[NotNullWhen(true)] out EntityUid? item, ItemSlotsComponent? itemSlots = null, bool excludeUserAudio = false)
|
[NotNullWhen(true)] out EntityUid? item, ItemSlotsComponent? itemSlots = null, bool excludeUserAudio = false)
|
||||||
{
|
{
|
||||||
item = null;
|
item = null;
|
||||||
@@ -586,4 +590,10 @@ namespace Content.Shared.Containers.ItemSlots
|
|||||||
args.State = new ItemSlotsComponentState(component.Slots);
|
args.State = new ItemSlotsComponentState(component.Slots);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised directed on an entity when one of its item slots changes.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public readonly struct ItemSlotChangedEvent {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Weapons.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class BoltActionBarrelComponentState : ComponentState
|
|
||||||
{
|
|
||||||
public (bool chambered, bool spent) Chamber { get; }
|
|
||||||
public FireRateSelector FireRateSelector { get; }
|
|
||||||
public (int count, int max)? Magazine { get; }
|
|
||||||
public string? SoundGunshot { get; }
|
|
||||||
|
|
||||||
public BoltActionBarrelComponentState(
|
|
||||||
(bool chambered, bool spent) chamber,
|
|
||||||
FireRateSelector fireRateSelector,
|
|
||||||
(int count, int max)? magazine,
|
|
||||||
string? soundGunshot)
|
|
||||||
{
|
|
||||||
Chamber = chamber;
|
|
||||||
FireRateSelector = fireRateSelector;
|
|
||||||
Magazine = magazine;
|
|
||||||
SoundGunshot = soundGunshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Weapons.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public enum AmmoVisuals
|
|
||||||
{
|
|
||||||
AmmoCount,
|
|
||||||
AmmoMax,
|
|
||||||
Spent,
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public enum MagazineBarrelVisuals
|
|
||||||
{
|
|
||||||
MagLoaded
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public enum BarrelBoltVisuals
|
|
||||||
{
|
|
||||||
BoltOpen,
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class MagazineBarrelComponentState : ComponentState
|
|
||||||
{
|
|
||||||
public bool Chambered { get; }
|
|
||||||
public FireRateSelector FireRateSelector { get; }
|
|
||||||
public (int count, int max)? Magazine { get; }
|
|
||||||
public string? SoundGunshot { get; }
|
|
||||||
|
|
||||||
public MagazineBarrelComponentState(
|
|
||||||
bool chambered,
|
|
||||||
FireRateSelector fireRateSelector,
|
|
||||||
(int count, int max)? magazine,
|
|
||||||
string? soundGunshot)
|
|
||||||
{
|
|
||||||
Chambered = chambered;
|
|
||||||
FireRateSelector = fireRateSelector;
|
|
||||||
Magazine = magazine;
|
|
||||||
SoundGunshot = soundGunshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Weapons.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class PumpBarrelComponentState : ComponentState
|
|
||||||
{
|
|
||||||
public (bool chambered, bool spent) Chamber { get; }
|
|
||||||
public FireRateSelector FireRateSelector { get; }
|
|
||||||
public (int count, int max)? Magazine { get; }
|
|
||||||
public string? SoundGunshot { get; }
|
|
||||||
|
|
||||||
public PumpBarrelComponentState(
|
|
||||||
(bool chambered, bool spent) chamber,
|
|
||||||
FireRateSelector fireRateSelector,
|
|
||||||
(int count, int max)? magazine,
|
|
||||||
string? soundGunshot)
|
|
||||||
{
|
|
||||||
Chamber = chamber;
|
|
||||||
FireRateSelector = fireRateSelector;
|
|
||||||
Magazine = magazine;
|
|
||||||
SoundGunshot = soundGunshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Content.Shared.Weapons.Ranged.Components;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Weapons.Ranged.Barrels.Components
|
|
||||||
{
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class RevolverBarrelComponentState : ComponentState
|
|
||||||
{
|
|
||||||
public int CurrentSlot { get; }
|
|
||||||
public FireRateSelector FireRateSelector { get; }
|
|
||||||
public bool?[] Bullets { get; }
|
|
||||||
public string? SoundGunshot { get; }
|
|
||||||
|
|
||||||
public RevolverBarrelComponentState(
|
|
||||||
int currentSlot,
|
|
||||||
FireRateSelector fireRateSelector,
|
|
||||||
bool?[] bullets,
|
|
||||||
string? soundGunshot)
|
|
||||||
{
|
|
||||||
CurrentSlot = currentSlot;
|
|
||||||
FireRateSelector = fireRateSelector;
|
|
||||||
Bullets = bullets;
|
|
||||||
SoundGunshot = soundGunshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs
Normal file
54
Content.Shared/Weapons/Ranged/Components/AmmoComponent.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using Content.Shared.Sound;
|
||||||
|
using Content.Shared.Weapons.Ranged.Systems;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows the entity to be fired from a gun.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Virtual]
|
||||||
|
public class AmmoComponent : Component, IShootable
|
||||||
|
{
|
||||||
|
// Muzzle flash stored on ammo because if we swap a gun to whatever we may want to override it.
|
||||||
|
|
||||||
|
[ViewVariables, DataField("muzzleFlash")]
|
||||||
|
public ResourcePath? MuzzleFlash = new ResourcePath("Objects/Weapons/Guns/Projectiles/projectiles.rsi/muzzle_bullet.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns another prototype to be shot instead of itself.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(AmmoComponent))]
|
||||||
|
public sealed class CartridgeAmmoComponent : AmmoComponent
|
||||||
|
{
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string Prototype = default!;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("spent")]
|
||||||
|
public bool Spent = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much the ammo spreads when shot, in degrees. Does nothing if count is 0.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("spread")]
|
||||||
|
public float Spread = 10f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many prototypes are spawned when shot.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("count")]
|
||||||
|
public int Count = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Caseless ammunition.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("deleteOnSpawn")]
|
||||||
|
public bool DeleteOnSpawn;
|
||||||
|
|
||||||
|
[ViewVariables, DataField("soundEject")]
|
||||||
|
public SoundSpecifier? EjectSound = new SoundCollectionSpecifier("CasingEject");
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
[NetworkedComponent]
|
||||||
|
public abstract class AmmoProviderComponent : Component {}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Content.Shared.Sound;
|
||||||
|
using Content.Shared.Whitelist;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed class BallisticAmmoProviderComponent : Component
|
||||||
|
{
|
||||||
|
[ViewVariables(VVAccess.ReadOnly), DataField("soundRack")]
|
||||||
|
public SoundSpecifier? SoundRack = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/smg_cock.ogg");
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadOnly), DataField("soundInsert")]
|
||||||
|
public SoundSpecifier? SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
|
||||||
|
|
||||||
|
[ViewVariables, DataField("proto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string? FillProto;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("capacity")]
|
||||||
|
public int Capacity = 30;
|
||||||
|
|
||||||
|
[ViewVariables, DataField("unspawnedCount")]
|
||||||
|
public int UnspawnedCount;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("whitelist")]
|
||||||
|
public EntityWhitelist? Whitelist;
|
||||||
|
|
||||||
|
public Container Container = default!;
|
||||||
|
|
||||||
|
// TODO: Make this use stacks when the typeserializer is done.
|
||||||
|
[ViewVariables, DataField("entities")]
|
||||||
|
public List<EntityUid> Entities = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Will the ammoprovider automatically cycle through rounds or does it need doing manually.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("autoCycle")]
|
||||||
|
public bool AutoCycle = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the gun ready to shoot; if AutoCycle is true then this will always stay true and not need to be manually done.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("cycled")]
|
||||||
|
public bool Cycled = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
public abstract class BatteryAmmoProviderComponent : AmmoProviderComponent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How much battery it costs to fire once.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("fireCost")]
|
||||||
|
public float FireCost = 100;
|
||||||
|
|
||||||
|
// Batteries aren't predicted which means we need to track the battery and manually count it ourselves woo!
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public int Shots;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public int Capacity;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chamber + mags in one package. If you need just magazine then use <see cref="MagazineAmmoProviderComponent"/>
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed class ChamberMagazineAmmoProviderComponent : MagazineAmmoProviderComponent {}
|
||||||
@@ -2,7 +2,7 @@ using Content.Shared.Sound;
|
|||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
namespace Content.Shared.Weapons.Ranged;
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plays a sound when its non-hard fixture collides with a player.
|
/// Plays a sound when its non-hard fixture collides with a player.
|
||||||
119
Content.Shared/Weapons/Ranged/Components/GunComponent.cs
Normal file
119
Content.Shared/Weapons/Ranged/Components/GunComponent.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using Content.Shared.Actions.ActionTypes;
|
||||||
|
using Content.Shared.Sound;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent, Virtual]
|
||||||
|
public class GunComponent : Component
|
||||||
|
{
|
||||||
|
#region Sound
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("soundGunshot")]
|
||||||
|
public SoundSpecifier? SoundGunshot = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/smg.ogg");
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("soundEmpty")]
|
||||||
|
public SoundSpecifier? SoundEmpty = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sound played when toggling the <see cref="SelectedMode"/> for this gun.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("soundMode")]
|
||||||
|
public SoundSpecifier? SoundModeToggle = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/selector.ogg");
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Recoil
|
||||||
|
|
||||||
|
// These values are very small for now until we get a debug overlay and fine tune it
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Last time the gun fired.
|
||||||
|
/// Used for recoil purposes.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("lastFire")]
|
||||||
|
public TimeSpan LastFire = TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What the current spread is for shooting. This gets changed every time the gun fires.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("currentAngle")]
|
||||||
|
public Angle CurrentAngle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much the spread increases every time the gun fires.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("angleIncrease")]
|
||||||
|
public Angle AngleIncrease = Angle.FromDegrees(0.5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much the <see cref="CurrentAngle"/> decreases per second.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("angleDecay")]
|
||||||
|
public Angle AngleDecay = Angle.FromDegrees(4);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum angle allowed for <see cref="CurrentAngle"/>
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("maxAngle")]
|
||||||
|
public Angle MaxAngle = Angle.FromDegrees(2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum angle allowed for <see cref="CurrentAngle"/>
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("minAngle")]
|
||||||
|
public Angle MinAngle = Angle.FromDegrees(1);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the gun is being requested to shoot.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public EntityCoordinates? ShootCoordinates = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used for tracking semi-auto / burst
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public int ShotCounter = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many times it shoots per second.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("fireRate")]
|
||||||
|
public float FireRate = 8f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the gun is next available to be shot.
|
||||||
|
/// Can be set multiple times in a single tick due to guns firing faster than a single tick time.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("nextFire")]
|
||||||
|
public TimeSpan NextFire = TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What firemodes can be selected.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("availableModes")]
|
||||||
|
public SelectiveFire AvailableModes = SelectiveFire.SemiAuto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What firemode is currently selected.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("selectedMode")]
|
||||||
|
public SelectiveFire SelectedMode = SelectiveFire.SemiAuto;
|
||||||
|
|
||||||
|
[DataField("selectModeAction")]
|
||||||
|
public InstantAction? SelectModeAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum SelectiveFire : byte
|
||||||
|
{
|
||||||
|
Invalid = 0,
|
||||||
|
// Combat mode already functions as the equivalent of Safety
|
||||||
|
SemiAuto = 1 << 0,
|
||||||
|
Burst = 1 << 1,
|
||||||
|
FullAuto = 1 << 2, // Not in the building!
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed class HitscanBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
|
||||||
|
{
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<HitscanPrototype>))]
|
||||||
|
public string Prototype = default!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Content.Shared.Sound;
|
||||||
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wrapper around a magazine (handled via ItemSlot). Passes all AmmoProvider logic onto it.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Virtual]
|
||||||
|
public class MagazineAmmoProviderComponent : AmmoProviderComponent
|
||||||
|
{
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("soundAutoEject")]
|
||||||
|
public SoundSpecifier? SoundAutoEject = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the magazine automatically eject when empty.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("autoEject")]
|
||||||
|
public bool AutoEject = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed class ProjectileBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
|
||||||
|
{
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string Prototype = default!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Content.Shared.Sound;
|
||||||
|
using Content.Shared.Whitelist;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed class RevolverAmmoProviderComponent : AmmoProviderComponent
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Revolver has an array of its slots of which we can fire from any index.
|
||||||
|
* We also keep a separate array of slots we haven't spawned entities for, Chambers. This means that rather than creating
|
||||||
|
* for example 7 entities when revolver spawns (1 for the revolver and 6 cylinders) we can instead defer it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[ViewVariables, DataField("whitelist")]
|
||||||
|
public EntityWhitelist? Whitelist;
|
||||||
|
|
||||||
|
public Container AmmoContainer = default!;
|
||||||
|
|
||||||
|
[ViewVariables, DataField("currentSlot")]
|
||||||
|
public int CurrentIndex;
|
||||||
|
|
||||||
|
[ViewVariables, DataField("capacity")]
|
||||||
|
public int Capacity = 6;
|
||||||
|
|
||||||
|
// Like BallisticAmmoProvider we defer spawning until necessary
|
||||||
|
// AmmoSlots is the instantiated ammo and Chambers is the unspawned ammo (that may or may not have been shot).
|
||||||
|
|
||||||
|
[DataField("ammoSlots")]
|
||||||
|
public EntityUid?[] AmmoSlots = Array.Empty<EntityUid?>();
|
||||||
|
|
||||||
|
[DataField("chambers")]
|
||||||
|
public bool?[] Chambers = Array.Empty<bool?>();
|
||||||
|
|
||||||
|
[DataField("proto", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string? FillPrototype = "CartridgeMagnum";
|
||||||
|
|
||||||
|
[ViewVariables, DataField("soundEject")]
|
||||||
|
public SoundSpecifier? SoundEject = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
|
||||||
|
|
||||||
|
[ViewVariables, DataField("soundInsert")]
|
||||||
|
public SoundSpecifier? SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
|
||||||
|
|
||||||
|
[ViewVariables, DataField("soundSpin")]
|
||||||
|
public SoundSpecifier? SoundSpin = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/revolver_spin.ogg");
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows an ItemStatus with the ammo of the gun. Adjusts based on what the ammoprovider is.
|
||||||
|
/// </summary>
|
||||||
|
[NetworkedComponent]
|
||||||
|
public abstract class SharedAmmoCounterComponent : Component {}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
namespace Content.Shared.Weapons.Ranged.Components
|
|
||||||
{
|
|
||||||
public abstract class SharedRangedBarrelComponent : Component
|
|
||||||
{
|
|
||||||
[ViewVariables]
|
|
||||||
public abstract FireRateSelector FireRateSelector { get; }
|
|
||||||
[ViewVariables]
|
|
||||||
public abstract FireRateSelector AllRateSelectors { get; }
|
|
||||||
[ViewVariables]
|
|
||||||
public abstract float FireRate { get; }
|
|
||||||
[ViewVariables]
|
|
||||||
public abstract int ShotsLeft { get; }
|
|
||||||
[ViewVariables]
|
|
||||||
public abstract int Capacity { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum FireRateSelector
|
|
||||||
{
|
|
||||||
Safety = 0,
|
|
||||||
Single = 1 << 0,
|
|
||||||
Automatic = 1 << 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using Robust.Shared.GameStates;
|
|
||||||
using Robust.Shared.Map;
|
|
||||||
using Robust.Shared.Serialization;
|
|
||||||
|
|
||||||
namespace Content.Shared.Weapons.Ranged.Components
|
|
||||||
{
|
|
||||||
[NetworkedComponent()]
|
|
||||||
public abstract class SharedRangedWeaponComponent : Component
|
|
||||||
{
|
|
||||||
// Each RangedWeapon should have a RangedWeapon component +
|
|
||||||
// some kind of RangedBarrelComponent (this dictates what ammo is retrieved).
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class RangedWeaponComponentState : ComponentState
|
|
||||||
{
|
|
||||||
public FireRateSelector FireRateSelector { get; }
|
|
||||||
|
|
||||||
public RangedWeaponComponentState(
|
|
||||||
FireRateSelector fireRateSelector
|
|
||||||
)
|
|
||||||
{
|
|
||||||
FireRateSelector = fireRateSelector;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An event raised when the weapon is fired at a position on the map by a client.
|
|
||||||
/// </summary>
|
|
||||||
[Serializable, NetSerializable]
|
|
||||||
public sealed class FirePosEvent : EntityEventArgs
|
|
||||||
{
|
|
||||||
public EntityCoordinates Coordinates;
|
|
||||||
|
|
||||||
public FirePosEvent(EntityCoordinates coordinates)
|
|
||||||
{
|
|
||||||
Coordinates = coordinates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
Content.Shared/Weapons/Ranged/Events/AmmoShotEvent.cs
Normal file
9
Content.Shared/Weapons/Ranged/Events/AmmoShotEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Content.Shared.Weapons.Ranged.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised on a gun when projectiles have been fired from it.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AmmoShotEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public List<EntityUid> FiredProjectiles = default!;
|
||||||
|
}
|
||||||
11
Content.Shared/Weapons/Ranged/Events/GetAmmoCountEvent.cs
Normal file
11
Content.Shared/Weapons/Ranged/Events/GetAmmoCountEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Content.Shared.Weapons.Ranged.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised on an AmmoProvider to request deets.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public struct GetAmmoCountEvent
|
||||||
|
{
|
||||||
|
public int Count;
|
||||||
|
public int Capacity;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared.Weapons.Ranged
|
namespace Content.Shared.Weapons.Ranged.Events
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is sent if the MagazineBarrel AutoEjects the magazine
|
/// This is sent if the MagazineBarrel AutoEjects the magazine
|
||||||
14
Content.Shared/Weapons/Ranged/Events/RequestShootEvent.cs
Normal file
14
Content.Shared/Weapons/Ranged/Events/RequestShootEvent.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised on the client to indicate it'd like to shoot.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class RequestShootEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid Gun;
|
||||||
|
public EntityCoordinates Coordinates;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised on the client to request it would like to stop hooting.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class RequestStopShootEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid Gun;
|
||||||
|
}
|
||||||
26
Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs
Normal file
26
Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised on a gun when it would like to take the specified amount of ammo.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TakeAmmoEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
public EntityUid? User;
|
||||||
|
public readonly int Shots;
|
||||||
|
public List<IShootable> Ammo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Coordinates to spawn the ammo at.
|
||||||
|
/// </summary>
|
||||||
|
public EntityCoordinates Coordinates;
|
||||||
|
|
||||||
|
public TakeAmmoEvent(int shots, List<IShootable> ammo, EntityCoordinates coordinates, EntityUid? user)
|
||||||
|
{
|
||||||
|
Shots = shots;
|
||||||
|
Ammo = ammo;
|
||||||
|
Coordinates = coordinates;
|
||||||
|
User = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Content.Shared/Weapons/Ranged/HitscanPrototype.cs
Normal file
39
Content.Shared/Weapons/Ranged/HitscanPrototype.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.Physics;
|
||||||
|
using Content.Shared.Weapons.Ranged.Systems;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Shared.Weapons.Ranged;
|
||||||
|
|
||||||
|
[Prototype("hitscan")]
|
||||||
|
public sealed class HitscanPrototype : IPrototype, IShootable
|
||||||
|
{
|
||||||
|
[ViewVariables]
|
||||||
|
[IdDataFieldAttribute]
|
||||||
|
public string ID { get; } = default!;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("damage")]
|
||||||
|
public DamageSpecifier? Damage;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadOnly), DataField("muzzleFlash")]
|
||||||
|
public SpriteSpecifier? MuzzleFlash;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadOnly), DataField("travelFlash")]
|
||||||
|
public SpriteSpecifier? TravelFlash;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadOnly), DataField("impactFlash")]
|
||||||
|
public SpriteSpecifier? ImpactFlash;
|
||||||
|
|
||||||
|
[ViewVariables, DataField("collisionMask")]
|
||||||
|
public int CollisionMask = (int) CollisionGroup.Opaque;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite), DataField("color")]
|
||||||
|
public Color Color = Color.White;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try not to set this too high.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, DataField("maxLength")]
|
||||||
|
public float MaxLength = 20f;
|
||||||
|
}
|
||||||
6
Content.Shared/Weapons/Ranged/IShootable.cs
Normal file
6
Content.Shared/Weapons/Ranged/IShootable.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Content.Shared.Weapons.Ranged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface that says this can be shot from a gun. Exists to facilitate hitscan OR prototype shooting.
|
||||||
|
/// </summary>
|
||||||
|
public interface IShootable {}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user