Add a LOT more dakka (#1033)
* Start adding flashy flash * Change slop Might give a smoother decline * flashy flash * Add flashbang and flash projectiles Bang bang bang pull my flash trigger * Add collision check to area flash * Flash cleanupo * flash.ogg mixed to mono * Adjusted flash curve again * Enhancing flashes with unshaded and lights and shit Still a WIP * Add the other ballistic gun types Re-organised some of the gun stuff so the powercell guns share the shooting code with the ballistic guns. * Re-merging branch with master Also fixed some visualizer bugs * Last cleanup Fixed some crashes Fixed Deckard sprite Fixed Hitscan effects Re-applied master changes Re-factor to using soundsystem Add some more audio effects * Cleanup flashes for merge Can put flashbangs in lockers so you don't get blinded Fix some bugs * Fix shotties Also removed some redundant code * Bulldoze some legacycode brrrrrrrrt * Fix clientignore warnings * Add the other Stunnable types to StunnableProjectile * Some gun refactoring * Removed extra visualizers * All casing ejections use the same code * Speed loaders can have their ammo pulled out * Bolt sound less loud * Stop ThrowController from throwing * Fix speed loader visuals * Update hitscan collision mask and fix typo * Cleanup * Fit hitscan and flashbang collisions * Use the new flags support * Update taser placeholder description * Update protonames per style guide * Add yaml flag support for gun firerates * Cleanup crew * Fix Audio up (components, audio file, + remove global sounds) * Add server-side recoil back-in (forgot that I was testing this client-side) * Add Flag support for fire-rate selectors * Wrong int you dolt * Fix AI conflicts Haha ranged bulldozer go BRR (I'll rewrite it after the other AI systems are done). * Mix bang.ogg from stereo to mono * Make sure serializer's reading for guns Fixes integration test * Change EntitySystem calls to use the static function Also removed the Pumpbarrel commented-out code * Change StunnableProjectile defaults to 0 * Fix taser paralyse Apparently removing defaults means you have to specify the values, whodathunkit * Add slowdown to stunnableprojectiles and fix tasers * Remove FlagsFor from gun components Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com> Co-authored-by: Víctor Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
@@ -85,8 +85,6 @@ namespace Content.Client
|
||||
"Multitool",
|
||||
"Wrench",
|
||||
"Crowbar",
|
||||
"HitscanWeapon",
|
||||
"ProjectileWeapon",
|
||||
"Projectile",
|
||||
"MeleeWeapon",
|
||||
"Storeable",
|
||||
@@ -100,8 +98,8 @@ namespace Content.Client
|
||||
"LightBulb",
|
||||
"Healing",
|
||||
"Catwalk",
|
||||
"BallisticMagazine",
|
||||
"BallisticBullet",
|
||||
"RangedMagazine",
|
||||
"Ammo",
|
||||
"HitscanWeaponCapacitor",
|
||||
"PowerCell",
|
||||
"WeaponCapacitorCharger",
|
||||
@@ -148,6 +146,13 @@ namespace Content.Client
|
||||
"Bucket",
|
||||
"Puddle",
|
||||
"CanSpill",
|
||||
"SpeedLoader",
|
||||
"Hitscan",
|
||||
"BoltActionBarrel",
|
||||
"PumpBarrel",
|
||||
"RevolverBarrel",
|
||||
"ExplosiveProjectile",
|
||||
"StunnableProjectile",
|
||||
"RandomPottedPlant",
|
||||
"CommunicationsConsole",
|
||||
"BarSign",
|
||||
@@ -167,6 +172,9 @@ namespace Content.Client
|
||||
"SecureEntityStorage",
|
||||
"PresetIdCard",
|
||||
"SolarControlConsole",
|
||||
"BatteryBarrel",
|
||||
"FlashExplosive",
|
||||
"FlashProjectile",
|
||||
"Utensil",
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Content.Shared.GameObjects.Components.Weapons;
|
||||
using Robust.Client.Graphics.Drawing;
|
||||
using Robust.Client.Graphics.Overlays;
|
||||
using Robust.Client.Interfaces.Graphics;
|
||||
using Robust.Client.Interfaces.Graphics.Overlays;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timers.Timer;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ClientFlashableComponent : SharedFlashableComponent
|
||||
{
|
||||
private CancellationTokenSource _cancelToken;
|
||||
private TimeSpan _startTime;
|
||||
private double _duration;
|
||||
private FlashOverlay _overlay;
|
||||
|
||||
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
|
||||
{
|
||||
if (curState == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
if (playerManager.LocalPlayer.ControlledEntity != Owner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newState = (FlashComponentState) curState;
|
||||
if (newState.Time == default)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Few things here:
|
||||
// 1. If a shorter duration flash is applied then don't do anything
|
||||
// 2. If the client-side time is later than when the flash should've ended don't do anything
|
||||
var currentTime = IoCManager.Resolve<IGameTiming>().CurTime.TotalSeconds;
|
||||
var newEndTime = newState.Time.TotalSeconds + newState.Duration;
|
||||
var currentEndTime = _startTime.TotalSeconds + _duration;
|
||||
|
||||
if (currentEndTime > newEndTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTime > newEndTime)
|
||||
{
|
||||
DisableOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
_startTime = newState.Time;
|
||||
_duration = newState.Duration;
|
||||
|
||||
EnableOverlay(newEndTime - currentTime);
|
||||
}
|
||||
|
||||
private void EnableOverlay(double duration)
|
||||
{
|
||||
// If the timer gets reset
|
||||
if (_overlay != null)
|
||||
{
|
||||
_overlay.Duration = _duration;
|
||||
_overlay.StartTime = _startTime;
|
||||
_cancelToken.Cancel();
|
||||
}
|
||||
else
|
||||
{
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
_overlay = new FlashOverlay(_duration);
|
||||
overlayManager.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
_cancelToken = new CancellationTokenSource();
|
||||
Timer.Spawn((int) duration * 1000, DisableOverlay, _cancelToken.Token);
|
||||
}
|
||||
|
||||
private void DisableOverlay()
|
||||
{
|
||||
if (_overlay == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var overlayManager = IoCManager.Resolve<IOverlayManager>();
|
||||
overlayManager.RemoveOverlay(_overlay.ID);
|
||||
_overlay = null;
|
||||
_cancelToken.Cancel();
|
||||
_cancelToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FlashOverlay : Overlay
|
||||
{
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
private readonly IGameTiming _timer;
|
||||
private readonly IClyde _displayManager;
|
||||
public TimeSpan StartTime { get; set; }
|
||||
public double Duration { get; set; }
|
||||
public FlashOverlay(double duration) : base(nameof(FlashOverlay))
|
||||
{
|
||||
_timer = IoCManager.Resolve<IGameTiming>();
|
||||
_displayManager = IoCManager.Resolve<IClyde>();
|
||||
StartTime = _timer.CurTime;
|
||||
Duration = duration;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleBase handle)
|
||||
{
|
||||
var elapsedTime = (_timer.CurTime - StartTime).TotalSeconds;
|
||||
if (elapsedTime > Duration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var screenHandle = (DrawingHandleScreen) handle;
|
||||
|
||||
screenHandle.DrawRect(
|
||||
new UIBox2(0.0f, 0.0f, _displayManager.ScreenSize.X, _displayManager.ScreenSize.Y),
|
||||
Color.White.WithAlpha(GetAlpha(elapsedTime / Duration))
|
||||
);
|
||||
}
|
||||
|
||||
private float GetAlpha(double ratio)
|
||||
{
|
||||
// Ideally you just want a smooth slope to finish it so it's not jarring at the end
|
||||
// By all means put in a better curve
|
||||
const float slope = -9.0f;
|
||||
const float exponent = 0.1f;
|
||||
const float yOffset = 9.0f;
|
||||
const float xOffset = 0.0f;
|
||||
|
||||
// Overkill but easy to adjust if you want to mess around with the design
|
||||
var result = (float) Math.Clamp(slope * (float) Math.Pow(ratio - xOffset, exponent) + yOffset, 0.0, 1.0);
|
||||
DebugTools.Assert(!float.IsNaN(result));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
public sealed class BallisticMagazineVisualizer2D : AppearanceVisualizer
|
||||
{
|
||||
private string _baseState;
|
||||
private int _steps;
|
||||
|
||||
public override void LoadData(YamlMappingNode node)
|
||||
{
|
||||
base.LoadData(node);
|
||||
|
||||
_baseState = node.GetNode("base_state").AsString();
|
||||
_steps = node.GetNode("steps").AsInt();
|
||||
}
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
var sprite = component.Owner.GetComponent<ISpriteComponent>();
|
||||
|
||||
if (!component.TryGetData(BallisticMagazineVisuals.AmmoCapacity, out int capacity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!component.TryGetData(BallisticMagazineVisuals.AmmoLeft, out int current))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var step = ContentHelpers.RoundToLevels(current, capacity, _steps);
|
||||
|
||||
sprite.LayerSetState(0, $"{_baseState}-{step}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
public sealed class BallisticMagazineWeaponVisualizer2D : AppearanceVisualizer
|
||||
{
|
||||
private string _baseState;
|
||||
private int _steps;
|
||||
|
||||
public override void LoadData(YamlMappingNode node)
|
||||
{
|
||||
base.LoadData(node);
|
||||
|
||||
_baseState = node.GetNode("base_state").AsString();
|
||||
_steps = node.GetNode("steps").AsInt();
|
||||
}
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
var sprite = component.Owner.GetComponent<ISpriteComponent>();
|
||||
|
||||
component.TryGetData(BallisticMagazineWeaponVisuals.MagazineLoaded, out bool loaded);
|
||||
|
||||
if (loaded)
|
||||
{
|
||||
if (!component.TryGetData(BallisticMagazineWeaponVisuals.AmmoCapacity, out int capacity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!component.TryGetData(BallisticMagazineWeaponVisuals.AmmoLeft, out int current))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// capacity is - 1 as normally a bullet is chambered so max state is virtually never hit.
|
||||
var step = ContentHelpers.RoundToLevels(current, capacity - 1, _steps);
|
||||
sprite.LayerSetState(0, $"{_baseState}-{step}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.LayerSetState(0, _baseState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,25 @@
|
||||
using System;
|
||||
using System;
|
||||
using Content.Client.Animations;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Client.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using static Content.Client.StaticIoC;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged.Barrels
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class BallisticMagazineWeaponComponent : Component, IItemStatus
|
||||
public class ClientMagazineBarrelComponent : Component, IItemStatus
|
||||
{
|
||||
private static readonly Animation AlarmAnimationSmg = new Animation
|
||||
{
|
||||
@@ -70,8 +67,8 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
}
|
||||
};
|
||||
|
||||
public override string Name => "BallisticMagazineWeapon";
|
||||
public override uint? NetID => ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON;
|
||||
public override string Name => "MagazineBarrel";
|
||||
public override uint? NetID => ContentNetIDs.MAGAZINE_BARREL;
|
||||
|
||||
private StatusControl _statusControl;
|
||||
|
||||
@@ -101,11 +98,11 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
|
||||
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
|
||||
{
|
||||
if (!(curState is BallisticMagazineWeaponComponentState cast))
|
||||
if (!(curState is MagazineBarrelComponentState cast))
|
||||
return;
|
||||
|
||||
Chambered = cast.Chambered;
|
||||
MagazineCount = cast.MagazineCount;
|
||||
MagazineCount = cast.Magazine;
|
||||
_statusControl?.Update();
|
||||
}
|
||||
|
||||
@@ -115,9 +112,10 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
|
||||
switch (message)
|
||||
{
|
||||
/*
|
||||
case BmwComponentAutoEjectedMessage _:
|
||||
_statusControl?.PlayAlarmAnimation();
|
||||
return;
|
||||
return;*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,13 +136,13 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
|
||||
private sealed class StatusControl : Control
|
||||
{
|
||||
private readonly BallisticMagazineWeaponComponent _parent;
|
||||
private readonly ClientMagazineBarrelComponent _parent;
|
||||
private readonly HBoxContainer _bulletsListTop;
|
||||
private readonly HBoxContainer _bulletsListBottom;
|
||||
private readonly TextureRect _chamberedBullet;
|
||||
private readonly Label _noMagazineLabel;
|
||||
|
||||
public StatusControl(BallisticMagazineWeaponComponent parent)
|
||||
public StatusControl(ClientMagazineBarrelComponent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand;
|
||||
@@ -181,7 +179,7 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
},
|
||||
(_chamberedBullet = new TextureRect
|
||||
{
|
||||
Texture = ResC.GetTexture("/Textures/UserInterface/status/bullets/chambered.png"),
|
||||
Texture = StaticIoC.ResC.GetTexture("/Textures/UserInterface/status/bullets/chambered.png"),
|
||||
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
||||
SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Fill,
|
||||
})
|
||||
@@ -223,7 +221,7 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
texturePath = "/Textures/UserInterface/status/bullets/tiny.png";
|
||||
}
|
||||
|
||||
var texture = ResC.GetTexture(texturePath);
|
||||
var texture = StaticIoC.ResC.GetTexture(texturePath);
|
||||
|
||||
const int tinyMaxRow = 60;
|
||||
|
||||
@@ -282,4 +280,4 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged.Barrels.Visualizers
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class BarrelBoltVisualizer2D : AppearanceVisualizer
|
||||
{
|
||||
public override void InitializeEntity(IEntity entity)
|
||||
{
|
||||
base.InitializeEntity(entity);
|
||||
var sprite = entity.GetComponent<ISpriteComponent>();
|
||||
sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, $"bolt-open");
|
||||
}
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
var sprite = component.Owner.GetComponent<ISpriteComponent>();
|
||||
|
||||
if (!component.TryGetData(BarrelBoltVisuals.BoltOpen, out bool boltOpen))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (boltOpen)
|
||||
{
|
||||
sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-open");
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.LayerSetState(RangedBarrelVisualLayers.Bolt, "bolt-closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Utility;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged.Barrels.Visualizers
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class MagVisualizer2D : AppearanceVisualizer
|
||||
{
|
||||
private bool _magLoaded;
|
||||
private string _magState;
|
||||
private int _magSteps;
|
||||
private bool _zeroVisible;
|
||||
|
||||
public override void LoadData(YamlMappingNode node)
|
||||
{
|
||||
base.LoadData(node);
|
||||
_magState = node.GetNode("magState").AsString();
|
||||
_magSteps = node.GetNode("steps").AsInt();
|
||||
_zeroVisible = node.GetNode("zeroVisible").AsBool();
|
||||
}
|
||||
|
||||
public override void InitializeEntity(IEntity entity)
|
||||
{
|
||||
base.InitializeEntity(entity);
|
||||
var sprite = entity.GetComponent<ISpriteComponent>();
|
||||
|
||||
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)
|
||||
{
|
||||
// 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 = component.Owner.GetComponent<ISpriteComponent>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged.Barrels.Visualizers
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class SpentAmmoVisualizer2D : AppearanceVisualizer
|
||||
{
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
var sprite = component.Owner.GetComponent<ISpriteComponent>();
|
||||
|
||||
if (!component.TryGetData(AmmoVisuals.Spent, out bool spent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sprite.LayerSetState(AmmoVisualLayers.Base, spent ? "spent" : "base");
|
||||
}
|
||||
}
|
||||
|
||||
public enum AmmoVisualLayers
|
||||
{
|
||||
Base,
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,35 @@ using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
// Yeah I put it all in the same enum, don't judge me
|
||||
public enum RangedBarrelVisualLayers
|
||||
{
|
||||
Base,
|
||||
BaseUnshaded,
|
||||
Bolt,
|
||||
Mag,
|
||||
MagUnshaded,
|
||||
}
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class ClientRangedWeaponComponent : SharedRangedWeaponComponent
|
||||
{
|
||||
public FireRateSelector FireRateSelector { get; private set; } = FireRateSelector.Safety;
|
||||
|
||||
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
if (!(curState is RangedWeaponComponentState rangedState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FireRateSelector = rangedState.FireRateSelector;
|
||||
}
|
||||
|
||||
public void SyncFirePos(GridCoordinates worldPos)
|
||||
{
|
||||
SendNetworkMessage(new SyncFirePosMessage(worldPos));
|
||||
SendNetworkMessage(new FirePosComponentMessage(worldPos));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Power
|
||||
{
|
||||
public class HitscanWeaponVisualizer2D : AppearanceVisualizer
|
||||
{
|
||||
private string _prefix;
|
||||
|
||||
public override void LoadData(YamlMappingNode node)
|
||||
{
|
||||
base.LoadData(node);
|
||||
|
||||
_prefix = node.GetNode("prefix").AsString();
|
||||
}
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
|
||||
var sprite = component.Owner.GetComponent<ISpriteComponent>();
|
||||
if (component.TryGetData(PowerCellVisuals.ChargeLevel, out float fraction))
|
||||
{
|
||||
sprite.LayerSetState(0, $"{_prefix}_{ContentHelpers.RoundToLevels(fraction, 1, 5) * 25}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using Content.Client.GameObjects.Components.Weapons.Ranged;
|
||||
using System;
|
||||
using Content.Client.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Client.Interfaces.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Robust.Client.GameObjects.EntitySystems;
|
||||
using Robust.Client.Interfaces.Graphics.ClientEye;
|
||||
using Robust.Client.Interfaces.Input;
|
||||
@@ -26,8 +28,8 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
|
||||
private InputSystem _inputSystem;
|
||||
private CombatModeSystem _combatModeSystem;
|
||||
private bool _isFirstShot;
|
||||
private bool _blocked;
|
||||
private int _shotCounter;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -46,18 +48,15 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var canFireSemi = _isFirstShot;
|
||||
|
||||
var state = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use);
|
||||
if (!_combatModeSystem.IsInCombatMode() || state != BoundKeyState.Down)
|
||||
{
|
||||
_isFirstShot = true;
|
||||
_shotCounter = 0;
|
||||
_blocked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_isFirstShot = false;
|
||||
|
||||
var entity = _playerManager.LocalPlayer.ControlledEntity;
|
||||
if (entity == null || !entity.TryGetComponent(out IHandsComponent hands))
|
||||
{
|
||||
@@ -71,6 +70,25 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
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;
|
||||
@@ -81,10 +99,7 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
if (!_mapManager.TryFindGridAt(worldPos, out var grid))
|
||||
grid = _mapManager.GetDefaultGrid(worldPos.MapId);
|
||||
|
||||
if (weapon.Automatic || canFireSemi)
|
||||
{
|
||||
weapon.SyncFirePos(grid.MapToGrid(worldPos));
|
||||
}
|
||||
weapon.SyncFirePos(grid.MapToGrid(worldPos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
using Content.Server.GameObjects;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
|
||||
namespace Content.Server.AI.Operators.Combat.Ranged
|
||||
{
|
||||
public class ShootAtEntityOperator : AiOperator
|
||||
{
|
||||
private IEntity _owner;
|
||||
private IEntity _target;
|
||||
private float _accuracy;
|
||||
|
||||
private float _burstTime;
|
||||
|
||||
private float _elapsedTime;
|
||||
|
||||
public ShootAtEntityOperator(IEntity owner, IEntity target, float accuracy, float burstTime = 0.5f)
|
||||
{
|
||||
_owner = owner;
|
||||
_target = target;
|
||||
_accuracy = accuracy;
|
||||
_burstTime = burstTime;
|
||||
}
|
||||
|
||||
public override bool TryStartup()
|
||||
{
|
||||
if (!base.TryStartup())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_owner.TryGetComponent(out CombatModeComponent combatModeComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!combatModeComponent.IsInCombatMode)
|
||||
{
|
||||
combatModeComponent.IsInCombatMode = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Shutdown(Outcome outcome)
|
||||
{
|
||||
base.Shutdown(outcome);
|
||||
if (_owner.TryGetComponent(out CombatModeComponent combatModeComponent))
|
||||
{
|
||||
combatModeComponent.IsInCombatMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override Outcome Execute(float frameTime)
|
||||
{
|
||||
// TODO: Probably just do all the checks on first try and then after that repeat the fire.
|
||||
if (_burstTime <= _elapsedTime)
|
||||
{
|
||||
return Outcome.Success;
|
||||
}
|
||||
|
||||
_elapsedTime += frameTime;
|
||||
|
||||
if (_target.TryGetComponent(out DamageableComponent damageableComponent))
|
||||
{
|
||||
if (damageableComponent.IsDead())
|
||||
{
|
||||
return Outcome.Success;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_owner.TryGetComponent(out HandsComponent hands) || hands.GetActiveHand == null)
|
||||
{
|
||||
return Outcome.Failed;
|
||||
}
|
||||
|
||||
var equippedWeapon = hands.GetActiveHand.Owner;
|
||||
|
||||
if ((_target.Transform.GridPosition.Position - _owner.Transform.GridPosition.Position).Length >
|
||||
_owner.GetComponent<AiControllerComponent>().VisionRadius)
|
||||
{
|
||||
// Not necessarily a hard fail, more of a soft fail
|
||||
return Outcome.Failed;
|
||||
}
|
||||
|
||||
// Unless RangedWeaponComponent is removed from hitscan weapons this shouldn't happen
|
||||
if (!equippedWeapon.TryGetComponent(out RangedWeaponComponent rangedWeaponComponent))
|
||||
{
|
||||
return Outcome.Failed;
|
||||
}
|
||||
|
||||
// TODO: Accuracy
|
||||
rangedWeaponComponent.AiFire(_owner, _target.Transform.GridPosition);
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Operators.Combat.Ranged
|
||||
{
|
||||
public class WaitForHitscanChargeOperator : AiOperator
|
||||
{
|
||||
private float _lastCharge = 0.0f;
|
||||
private float _lastFill = 0.0f;
|
||||
private HitscanWeaponComponent _hitscan;
|
||||
|
||||
public WaitForHitscanChargeOperator(IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_hitscan = hitscanWeaponComponent;
|
||||
}
|
||||
|
||||
public override Outcome Execute(float frameTime)
|
||||
{
|
||||
if (_hitscan.CapacitorComponent.Capacity - _hitscan.CapacitorComponent.Charge < 0.01f)
|
||||
{
|
||||
return Outcome.Success;
|
||||
}
|
||||
|
||||
// If we're not charging then just stop
|
||||
_lastFill = _hitscan.CapacitorComponent.Charge - _lastCharge;
|
||||
_lastCharge = _hitscan.CapacitorComponent.Charge;
|
||||
|
||||
if (_lastFill == 0.0f)
|
||||
{
|
||||
return Outcome.Failed;
|
||||
}
|
||||
return Outcome.Continuing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Inventory;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Melee;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.Considerations.Inventory;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
@@ -41,9 +40,6 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new MeleeWeaponEquippedCon(
|
||||
new InverseBoolCurve()),
|
||||
// We'll prioritise equipping ranged weapons; If we try and score this then it'll just keep swapping between ranged and melee
|
||||
new RangedWeaponEquippedCon(
|
||||
new InverseBoolCurve()),
|
||||
new CanPutTargetInHandsCon(
|
||||
new BoolCurve()),
|
||||
new MeleeWeaponSpeedCon(
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Combat.Ranged;
|
||||
using Content.Server.AI.Operators.Movement;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.Considerations.Movement;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class BallisticAttackEntity : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
private MoveToEntityOperator _moveOperator;
|
||||
|
||||
public BallisticAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
if (_moveOperator != null)
|
||||
{
|
||||
_moveOperator.MovedATile -= InLos;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
_moveOperator = new MoveToEntityOperator(Owner, _entity);
|
||||
_moveOperator.MovedATile += InLos;
|
||||
|
||||
// TODO: Accuracy in blackboard
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
_moveOperator,
|
||||
new ShootAtEntityOperator(Owner, _entity, 0.7f),
|
||||
});
|
||||
|
||||
// We will do a quick check now to see if we even need to move which also saves a pathfind
|
||||
InLos();
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
context.GetState<MoveTargetState>().SetValue(_entity);
|
||||
var equipped = context.GetState<EquippedEntityState>().GetValue();
|
||||
context.GetState<WeaponEntityState>().SetValue(equipped);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
// Check if we have a weapon; easy-out
|
||||
new BallisticWeaponEquippedCon(
|
||||
new BoolCurve()),
|
||||
new BallisticAmmoCon(
|
||||
new QuadraticCurve(1.0f, 0.15f, 0.0f, 0.0f)),
|
||||
// Don't attack a dead target
|
||||
new TargetIsDeadCon(
|
||||
new InverseBoolCurve()),
|
||||
// Deprioritise a target in crit
|
||||
new TargetIsCritCon(
|
||||
new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)),
|
||||
// Somewhat prioritise distance
|
||||
new DistanceCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.07f, 0.0f)),
|
||||
// Prefer weaker targets
|
||||
new TargetHealthCon(
|
||||
new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)),
|
||||
};
|
||||
|
||||
private void InLos()
|
||||
{
|
||||
// This should only be called if the movement operator is the current one;
|
||||
// if that turns out not to be the case we can just add a check here.
|
||||
if (Visibility.InLineOfSight(Owner, _entity))
|
||||
{
|
||||
_moveOperator.HaveArrived();
|
||||
var mover = ActionOperators.Dequeue();
|
||||
mover.Shutdown(Outcome.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Inventory;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.Considerations.Inventory;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class DropEmptyBallistic : UtilityAction
|
||||
{
|
||||
public sealed override float Bonus => 20.0f;
|
||||
private IEntity _entity;
|
||||
|
||||
public DropEmptyBallistic(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
new EquipEntityOperator(Owner, _entity),
|
||||
new DropEntityOperator(Owner, _entity)
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
context.GetState<WeaponEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new TargetInOurInventoryCon(
|
||||
new BoolCurve()),
|
||||
// Need to put in hands to drop
|
||||
new CanPutTargetInHandsCon(
|
||||
new BoolCurve()),
|
||||
// Drop that sucker
|
||||
new BallisticAmmoCon(
|
||||
new InverseBoolCurve()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Inventory;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Melee;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.Considerations.Inventory;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class EquipBallistic : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
|
||||
public EquipBallistic(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
new EquipEntityOperator(Owner, _entity)
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<WeaponEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new EquippedBallisticCon(
|
||||
new InverseBoolCurve()),
|
||||
new MeleeWeaponEquippedCon(
|
||||
new QuadraticCurve(0.9f, 1.0f, 0.1f, 0.0f)),
|
||||
new CanPutTargetInHandsCon(
|
||||
new BoolCurve()),
|
||||
new BallisticAmmoCon(
|
||||
new QuadraticCurve(1.0f, 0.15f, 0.0f, 0.0f)),
|
||||
new RangedWeaponFireRateCon(
|
||||
new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using Content.Server.AI.Operators.Sequences;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Containers;
|
||||
using Content.Server.AI.Utility.Considerations.Hands;
|
||||
using Content.Server.AI.Utility.Considerations.Movement;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class PickUpAmmo : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
|
||||
public PickUpAmmo(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<MoveTargetState>().SetValue(_entity);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
//TODO: Consider ammo's type and what guns we have
|
||||
new TargetAccessibleCon(
|
||||
new BoolCurve()),
|
||||
new FreeHandCon(
|
||||
new BoolCurve()),
|
||||
new DistanceCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
using Content.Server.AI.Operators.Sequences;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.Considerations.Containers;
|
||||
using Content.Server.AI.Utility.Considerations.Hands;
|
||||
using Content.Server.AI.Utility.Considerations.Movement;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class PickUpBallisticMagWeapon : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
|
||||
public PickUpBallisticMagWeapon(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<MoveTargetState>().SetValue(_entity);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
context.GetState<WeaponEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new HeldRangedWeaponsCon(
|
||||
new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)),
|
||||
new TargetAccessibleCon(
|
||||
new BoolCurve()),
|
||||
new FreeHandCon(
|
||||
new BoolCurve()),
|
||||
// For now don't grab empty guns - at least until we can start storing stuff in inventory
|
||||
new BallisticAmmoCon(
|
||||
new BoolCurve()),
|
||||
new DistanceCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
|
||||
new RangedWeaponFireRateCon(
|
||||
new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
|
||||
// TODO: Ballistic accuracy? Depends how the design transitions
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Inventory;
|
||||
using Content.Server.AI.Operators.Movement;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utility.Considerations.Movement;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Content.Server.GameObjects.Components.Power.Chargers;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class PutHitscanInCharger : UtilityAction
|
||||
{
|
||||
// Maybe a bad idea to not allow override
|
||||
public override bool CanOverride => false;
|
||||
private readonly IEntity _charger;
|
||||
|
||||
public PutHitscanInCharger(IEntity owner, IEntity charger, float weight) : base(owner)
|
||||
{
|
||||
_charger = charger;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
var weapon = context.GetState<EquippedEntityState>().GetValue();
|
||||
|
||||
if (weapon == null || _charger.GetComponent<WeaponCapacitorChargerComponent>().HeldItem != null)
|
||||
{
|
||||
ActionOperators = new Queue<AiOperator>();
|
||||
return;
|
||||
}
|
||||
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
new MoveToEntityOperator(Owner, _charger),
|
||||
new InteractWithEntityOperator(Owner, _charger),
|
||||
// Separate task will deal with picking it up
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<MoveTargetState>().SetValue(_charger);
|
||||
context.GetState<TargetEntityState>().SetValue(_charger);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } =
|
||||
{
|
||||
new HitscanWeaponEquippedCon(
|
||||
new BoolCurve()),
|
||||
new HitscanChargerFullCon(
|
||||
new InverseBoolCurve()),
|
||||
new HitscanChargerRateCon(
|
||||
new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
|
||||
new DistanceCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
|
||||
new HitscanChargeCon(
|
||||
new QuadraticCurve(-1.2f, 2.0f, 1.2f, 0.0f)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Inventory;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utility.Considerations.Inventory;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class DropEmptyHitscan : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
|
||||
public DropEmptyHitscan(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
new EquipEntityOperator(Owner, _entity),
|
||||
new DropEntityOperator(Owner, _entity)
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
context.GetState<WeaponEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new TargetInOurInventoryCon(
|
||||
new BoolCurve()),
|
||||
// Need to put in hands to drop
|
||||
new CanPutTargetInHandsCon(
|
||||
new BoolCurve()),
|
||||
// If completely empty then drop that sucker
|
||||
new HitscanChargeCon(
|
||||
new InverseBoolCurve()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Inventory;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Melee;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utility.Considerations.Inventory;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class EquipHitscan : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
|
||||
public EquipHitscan(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
new EquipEntityOperator(Owner, _entity)
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<WeaponEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new EquippedHitscanCon(
|
||||
new InverseBoolCurve()),
|
||||
new MeleeWeaponEquippedCon(
|
||||
new QuadraticCurve(0.9f, 1.0f, 0.1f, 0.0f)),
|
||||
new CanPutTargetInHandsCon(
|
||||
new BoolCurve()),
|
||||
new HitscanChargeCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.0f, 0.0f)),
|
||||
new RangedWeaponFireRateCon(
|
||||
new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
|
||||
new HitscanWeaponDamageCon(
|
||||
new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Combat.Ranged;
|
||||
using Content.Server.AI.Operators.Movement;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utility.Considerations.Movement;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class HitscanAttackEntity : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
private MoveToEntityOperator _moveOperator;
|
||||
|
||||
public HitscanAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
if (_moveOperator != null)
|
||||
{
|
||||
_moveOperator.MovedATile -= InLos;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
_moveOperator = new MoveToEntityOperator(Owner, _entity);
|
||||
_moveOperator.MovedATile += InLos;
|
||||
|
||||
// TODO: Accuracy in blackboard
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
_moveOperator,
|
||||
new ShootAtEntityOperator(Owner, _entity, 0.7f),
|
||||
});
|
||||
|
||||
InLos();
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
context.GetState<MoveTargetState>().SetValue(_entity);
|
||||
var equipped = context.GetState<EquippedEntityState>().GetValue();
|
||||
context.GetState<WeaponEntityState>().SetValue(equipped);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
// Check if we have a weapon; easy-out
|
||||
new HitscanWeaponEquippedCon(
|
||||
new BoolCurve()),
|
||||
new HitscanChargeCon(
|
||||
new QuadraticCurve(1.0f, 0.1f, 0.0f, 0.0f)),
|
||||
// Don't attack a dead target
|
||||
new TargetIsDeadCon(
|
||||
new InverseBoolCurve()),
|
||||
// Deprioritise a target in crit
|
||||
new TargetIsCritCon(
|
||||
new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)),
|
||||
// Somewhat prioritise distance
|
||||
new DistanceCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.07f, 0.0f)),
|
||||
// Prefer weaker targets
|
||||
new TargetHealthCon(
|
||||
new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)),
|
||||
};
|
||||
|
||||
private void InLos()
|
||||
{
|
||||
// This should only be called if the movement operator is the current one;
|
||||
// if that turns out not to be the case we can just add a check here.
|
||||
if (Visibility.InLineOfSight(Owner, _entity))
|
||||
{
|
||||
_moveOperator.HaveArrived();
|
||||
var mover = ActionOperators.Dequeue();
|
||||
mover.Shutdown(Outcome.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Operators;
|
||||
using Content.Server.AI.Operators.Combat.Ranged;
|
||||
using Content.Server.AI.Operators.Inventory;
|
||||
using Content.Server.AI.Operators.Movement;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utility.Considerations.Containers;
|
||||
using Content.Server.AI.Utility.Considerations.Hands;
|
||||
using Content.Server.AI.Utility.Considerations.Movement;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class PickUpHitscanFromCharger : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
private IEntity _charger;
|
||||
|
||||
public PickUpHitscanFromCharger(IEntity owner, IEntity entity, IEntity charger, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
_charger = charger;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new Queue<AiOperator>(new AiOperator[]
|
||||
{
|
||||
new MoveToEntityOperator(Owner, _charger),
|
||||
new WaitForHitscanChargeOperator(_entity),
|
||||
new PickupEntityOperator(Owner, _entity),
|
||||
});
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<MoveTargetState>().SetValue(_entity);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new HeldRangedWeaponsCon(
|
||||
new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)),
|
||||
new TargetAccessibleCon(
|
||||
new BoolCurve()),
|
||||
new FreeHandCon(
|
||||
new BoolCurve()),
|
||||
new DistanceCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
|
||||
// TODO: ChargerHasPower
|
||||
new RangedWeaponFireRateCon(
|
||||
new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
|
||||
new HitscanWeaponDamageCon(
|
||||
new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using Content.Server.AI.Operators.Sequences;
|
||||
using Content.Server.AI.Utility.Considerations;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utility.Considerations.Containers;
|
||||
using Content.Server.AI.Utility.Considerations.Hands;
|
||||
using Content.Server.AI.Utility.Considerations.Movement;
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.AI.WorldState.States.Movement;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class PickUpHitscanWeapon : UtilityAction
|
||||
{
|
||||
private IEntity _entity;
|
||||
|
||||
public PickUpHitscanWeapon(IEntity owner, IEntity entity, float weight) : base(owner)
|
||||
{
|
||||
_entity = entity;
|
||||
Bonus = weight;
|
||||
}
|
||||
|
||||
public override void SetupOperators(Blackboard context)
|
||||
{
|
||||
ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
|
||||
}
|
||||
|
||||
protected override void UpdateBlackboard(Blackboard context)
|
||||
{
|
||||
base.UpdateBlackboard(context);
|
||||
context.GetState<MoveTargetState>().SetValue(_entity);
|
||||
context.GetState<TargetEntityState>().SetValue(_entity);
|
||||
context.GetState<WeaponEntityState>().SetValue(_entity);
|
||||
}
|
||||
|
||||
protected override Consideration[] Considerations { get; } = {
|
||||
new HeldRangedWeaponsCon(
|
||||
new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)),
|
||||
new TargetAccessibleCon(
|
||||
new BoolCurve()),
|
||||
new FreeHandCon(
|
||||
new BoolCurve()),
|
||||
// For now don't grab empty guns - at least until we can start storing stuff in inventory
|
||||
new HitscanChargeCon(
|
||||
new BoolCurve()),
|
||||
new DistanceCon(
|
||||
new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
|
||||
// TODO: Weapon charge level
|
||||
new RangedWeaponFireRateCon(
|
||||
new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
|
||||
new HitscanWeaponDamageCon(
|
||||
new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utility.ExpandableActions.Combat;
|
||||
using Content.Server.AI.Utility.ExpandableActions.Combat.Melee;
|
||||
using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged;
|
||||
using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.Utility.BehaviorSets
|
||||
@@ -18,17 +10,9 @@ namespace Content.Server.AI.Utility.BehaviorSets
|
||||
{
|
||||
Actions = new IAiUtility[]
|
||||
{
|
||||
new PickUpRangedExp(),
|
||||
// TODO: Reload Ballistic
|
||||
new DropEmptyBallisticExp(),
|
||||
// TODO: Ideally long-term we should just store the weapons in backpack
|
||||
new DropEmptyHitscanExp(),
|
||||
new EquipMeleeExp(),
|
||||
new EquipBallisticExp(),
|
||||
new EquipHitscanExp(),
|
||||
new PickUpHitscanFromChargersExp(),
|
||||
new ChargeEquippedHitscanExp(),
|
||||
new RangedAttackNearbySpeciesExp(),
|
||||
new PickUpMeleeWeaponExp(),
|
||||
new MeleeAttackNearbySpeciesExp(),
|
||||
};
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic
|
||||
{
|
||||
public class BallisticAmmoCon : Consideration
|
||||
{
|
||||
public BallisticAmmoCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var weapon = context.GetState<WeaponEntityState>().GetValue();
|
||||
|
||||
if (weapon == null || !weapon.TryGetComponent(out BallisticMagazineWeaponComponent ballistic))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
var contained = ballistic.MagazineSlot.ContainedEntity;
|
||||
|
||||
if (contained == null)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
var mag = contained.GetComponent<BallisticMagazineComponent>();
|
||||
|
||||
if (mag.CountLoaded == 0)
|
||||
{
|
||||
// TODO: Do this better
|
||||
return ballistic.GetChambered(0) != null ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
return (float) mag.CountLoaded / mag.Capacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic
|
||||
{
|
||||
public class BallisticWeaponEquippedCon : Consideration
|
||||
{
|
||||
public BallisticWeaponEquippedCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var equipped = context.GetState<EquippedEntityState>().GetValue();
|
||||
|
||||
if (equipped == null)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Maybe change this to BallisticMagazineWeapon
|
||||
return equipped.HasComponent<BallisticMagazineWeaponComponent>() ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic
|
||||
{
|
||||
public class EquippedBallisticCon : Consideration
|
||||
{
|
||||
public EquippedBallisticCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var equipped = context.GetState<EquippedEntityState>().GetValue();
|
||||
|
||||
if (equipped == null || !equipped.HasComponent<BallisticMagazineWeaponComponent>())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
|
||||
{
|
||||
public class HasTargetLosCon : Consideration
|
||||
{
|
||||
public HasTargetLosCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
var target = context.GetState<TargetEntityState>().GetValue();
|
||||
if (target == null)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return Visibility.InLineOfSight(owner, target) ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Melee;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
|
||||
{
|
||||
public sealed class HeldRangedWeaponsCon : Consideration
|
||||
{
|
||||
public HeldRangedWeaponsCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var count = 0;
|
||||
const int max = 3;
|
||||
|
||||
foreach (var item in context.GetState<InventoryState>().GetValue())
|
||||
{
|
||||
if (item.HasComponent<RangedWeaponComponent>())
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return (float) count / max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class EquippedHitscanCon : Consideration
|
||||
{
|
||||
public EquippedHitscanCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var equipped = context.GetState<EquippedEntityState>().GetValue();
|
||||
|
||||
if (equipped == null || !equipped.HasComponent<HitscanWeaponComponent>())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class HitscanChargeCon : Consideration
|
||||
{
|
||||
public HitscanChargeCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var weapon = context.GetState<WeaponEntityState>().GetValue();
|
||||
|
||||
if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return hitscanWeaponComponent.CapacitorComponent.Charge / hitscanWeaponComponent.CapacitorComponent.Capacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.GameObjects.Components.Power.Chargers;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class HitscanChargerFullCon : Consideration
|
||||
{
|
||||
public HitscanChargerFullCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var target = context.GetState<TargetEntityState>().GetValue();
|
||||
|
||||
if (target == null ||
|
||||
!target.TryGetComponent(out WeaponCapacitorChargerComponent chargerComponent) ||
|
||||
chargerComponent.HeldItem != null)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.GameObjects.Components.Power.Chargers;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class HitscanChargerRateCon : Consideration
|
||||
{
|
||||
public HitscanChargerRateCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var target = context.GetState<TargetEntityState>().GetValue();
|
||||
if (target == null || !target.TryGetComponent(out WeaponCapacitorChargerComponent weaponCharger))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// AI don't care about efficiency, psfft!
|
||||
return weaponCharger.TransferRatio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class HitscanWeaponDamageCon : Consideration
|
||||
{
|
||||
public HitscanWeaponDamageCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var weapon = context.GetState<WeaponEntityState>().GetValue();
|
||||
|
||||
if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// Just went with max health
|
||||
return hitscanWeaponComponent.Damage / 300.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class HitscanWeaponEquippedCon : Consideration
|
||||
{
|
||||
public HitscanWeaponEquippedCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var equipped = context.GetState<EquippedEntityState>().GetValue();
|
||||
|
||||
if (equipped == null)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return equipped.HasComponent<HitscanWeaponComponent>() ? 1.0f : 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
|
||||
{
|
||||
public sealed class RangedWeaponEquippedCon : Consideration
|
||||
{
|
||||
public RangedWeaponEquippedCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var equipped = context.GetState<EquippedEntityState>().GetValue();
|
||||
|
||||
if (equipped == null || !equipped.HasComponent<RangedWeaponComponent>())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.AI.Utility.Curves;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States.Combat;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged;
|
||||
|
||||
namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
|
||||
{
|
||||
public class RangedWeaponFireRateCon : Consideration
|
||||
{
|
||||
public RangedWeaponFireRateCon(IResponseCurve curve) : base(curve) {}
|
||||
|
||||
public override float GetScore(Blackboard context)
|
||||
{
|
||||
var weapon = context.GetState<WeaponEntityState>().GetValue();
|
||||
|
||||
if (weapon == null || !weapon.TryGetComponent(out RangedWeaponComponent ranged))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
return ranged.FireRate / 100.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class DropEmptyBallisticExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
|
||||
foreach (var entity in context.GetState<InventoryState>().GetValue())
|
||||
{
|
||||
if (entity.HasComponent<BallisticMagazineWeaponComponent>())
|
||||
{
|
||||
yield return new DropEmptyBallistic(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class EquipBallisticExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
|
||||
foreach (var entity in context.GetState<InventoryState>().GetValue())
|
||||
{
|
||||
yield return new EquipBallistic(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic
|
||||
{
|
||||
public sealed class PickUpAmmoExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
if (!owner.TryGetComponent(out AiControllerComponent controller))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(BallisticMagazineComponent),
|
||||
controller.VisionRadius))
|
||||
{
|
||||
yield return new PickUpAmmo(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.Components.Power.Chargers;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class ChargeEquippedHitscanExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
if (!owner.TryGetComponent(out AiControllerComponent controller))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(WeaponCapacitorChargerComponent),
|
||||
controller.VisionRadius))
|
||||
{
|
||||
yield return new PutHitscanInCharger(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public class DropEmptyHitscanExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
|
||||
foreach (var entity in context.GetState<InventoryState>().GetValue())
|
||||
{
|
||||
if (entity.HasComponent<HitscanWeaponComponent>())
|
||||
{
|
||||
yield return new DropEmptyHitscan(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Inventory;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class EquipHitscanExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
|
||||
foreach (var entity in context.GetState<InventoryState>().GetValue())
|
||||
{
|
||||
yield return new EquipHitscan(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.Components.Power.Chargers;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan
|
||||
{
|
||||
public sealed class PickUpHitscanFromChargersExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
if (!owner.TryGetComponent(out AiControllerComponent controller))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(WeaponCapacitorChargerComponent),
|
||||
controller.VisionRadius))
|
||||
{
|
||||
var contained = entity.GetComponent<WeaponCapacitorChargerComponent>().HeldItem;
|
||||
|
||||
if (contained != null)
|
||||
{
|
||||
yield return new PickUpHitscanFromCharger(owner, entity, contained, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Combat.Nearby;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged
|
||||
{
|
||||
public sealed class PickUpRangedExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatPrepBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
|
||||
foreach (var entity in context.GetState<NearbyRangedWeapons>().GetValue())
|
||||
{
|
||||
if (entity.HasComponent<HitscanWeaponComponent>())
|
||||
{
|
||||
yield return new PickUpHitscanWeapon(owner, entity, Bonus);
|
||||
}
|
||||
|
||||
if (entity.HasComponent<BallisticMagazineWeaponComponent>())
|
||||
{
|
||||
yield return new PickUpBallisticMagWeapon(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utility.Actions;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
|
||||
using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
|
||||
using Content.Server.AI.WorldState;
|
||||
using Content.Server.AI.WorldState.States;
|
||||
using Content.Server.AI.WorldState.States.Mobs;
|
||||
|
||||
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged
|
||||
{
|
||||
public sealed class RangedAttackNearbySpeciesExp : ExpandableUtilityAction
|
||||
{
|
||||
public override float Bonus => UtilityAction.CombatBonus;
|
||||
|
||||
public override IEnumerable<UtilityAction> GetActions(Blackboard context)
|
||||
{
|
||||
var owner = context.GetState<SelfState>().GetValue();
|
||||
|
||||
foreach (var entity in context.GetState<NearbySpeciesState>().GetValue())
|
||||
{
|
||||
yield return new HitscanAttackEntity(owner, entity, Bonus);
|
||||
yield return new BallisticAttackEntity(owner, entity, Bonus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.Components.Power.Chargers;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.WorldState.States.Combat.Nearby
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class NearbyLaserChargersState : StateData<List<IEntity>>
|
||||
{
|
||||
public override string Name => "NearbyLaserChargers";
|
||||
|
||||
public override List<IEntity> GetValue()
|
||||
{
|
||||
var nearby = new List<IEntity>();
|
||||
|
||||
if (!Owner.TryGetComponent(out AiControllerComponent controller))
|
||||
{
|
||||
return nearby;
|
||||
}
|
||||
|
||||
foreach (var result in Visibility
|
||||
.GetNearestEntities(Owner.Transform.GridPosition, typeof(WeaponCapacitorChargerComponent), controller.VisionRadius))
|
||||
{
|
||||
nearby.Add(result);
|
||||
}
|
||||
|
||||
return nearby;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.WorldState.States.Combat.Nearby
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class NearbyLaserWeapons : StateData<List<IEntity>>
|
||||
{
|
||||
public override string Name => "NearbyLaserWeapons";
|
||||
|
||||
public override List<IEntity> GetValue()
|
||||
{
|
||||
var result = new List<IEntity>();
|
||||
|
||||
if (!Owner.TryGetComponent(out AiControllerComponent controller))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var entity in Visibility
|
||||
.GetNearestEntities(Owner.Transform.GridPosition, typeof(HitscanWeaponComponent), controller.VisionRadius))
|
||||
{
|
||||
result.Add(entity);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.AI.Utils;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.AI.WorldState.States.Combat.Nearby
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class NearbyRangedWeapons : CachedStateData<List<IEntity>>
|
||||
{
|
||||
public override string Name => "NearbyRangedWeapons";
|
||||
|
||||
protected override List<IEntity> GetTrueValue()
|
||||
{
|
||||
var result = new List<IEntity>();
|
||||
|
||||
if (!Owner.TryGetComponent(out AiControllerComponent controller))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var entity in Visibility
|
||||
.GetNearestEntities(Owner.Transform.GridPosition, typeof(RangedWeaponComponent), controller.VisionRadius))
|
||||
{
|
||||
result.Add(entity);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.AI.WorldState.States.Combat.Ranged
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class Accuracy : StateData<float>
|
||||
{
|
||||
public override string Name => "Accuracy";
|
||||
|
||||
public override float GetValue()
|
||||
{
|
||||
// TODO: Maybe just make it a SetValue (maybe make a third type besides sensor / daemon called settablestate)
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.AI.WorldState.States.Combat.Ranged
|
||||
{
|
||||
/// <summary>
|
||||
/// How long to wait between bursts
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class BurstCooldown : PlanningStateData<float>
|
||||
{
|
||||
public override string Name => "BurstCooldown";
|
||||
public override void Reset()
|
||||
{
|
||||
Value = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using Content.Server.GameObjects;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.AI.WorldState.States.Combat.Ranged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the discrete ammo count
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class EquippedRangedWeaponAmmo : StateData<int?>
|
||||
{
|
||||
public override string Name => "EquippedRangedWeaponAmmo";
|
||||
|
||||
public override int? GetValue()
|
||||
{
|
||||
if (!Owner.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var equippedItem = handsComponent.GetActiveHand?.Owner;
|
||||
if (equippedItem == null) return null;
|
||||
|
||||
if (equippedItem.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
|
||||
{
|
||||
return (int) hitscanWeaponComponent.CapacitorComponent.Charge / hitscanWeaponComponent.BaseFireCost;
|
||||
}
|
||||
|
||||
if (equippedItem.TryGetComponent(out BallisticMagazineWeaponComponent ballisticComponent))
|
||||
{
|
||||
return ballisticComponent.MagazineSlot.ContainedEntities.Count;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.AI.WorldState.States.Combat.Ranged
|
||||
{
|
||||
/// <summary>
|
||||
/// How many shots to take before cooling down
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class MaxBurstCount : PlanningStateData<int>
|
||||
{
|
||||
public override string Name => "BurstCount";
|
||||
public override void Reset()
|
||||
{
|
||||
Value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Content.Server.GameObjects.Components.Weapon;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Explosion
|
||||
{
|
||||
/// <summary>
|
||||
/// When triggered will flash in an area around the object and destroy itself
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class FlashExplosiveComponent : Component, ITimerTrigger, IDestroyAct
|
||||
{
|
||||
public override string Name => "FlashExplosive";
|
||||
|
||||
private float _range;
|
||||
private double _duration;
|
||||
private string _sound;
|
||||
private bool _deleteOnFlash;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _range, "range", 7.0f);
|
||||
serializer.DataField(ref _duration, "duration", 8.0);
|
||||
serializer.DataField(ref _sound, "sound", "/Audio/effects/flash_bang.ogg");
|
||||
serializer.DataField(ref _deleteOnFlash, "deleteOnFlash", true);
|
||||
}
|
||||
|
||||
public bool Explode()
|
||||
{
|
||||
// If we're in a locker or whatever then can't flash anything
|
||||
ContainerHelpers.TryGetContainer(Owner, out var container);
|
||||
if (container == null || !container.Owner.HasComponent<EntityStorageComponent>())
|
||||
{
|
||||
ServerFlashableComponent.FlashAreaHelper(Owner, _range, _duration);
|
||||
}
|
||||
|
||||
if (_sound != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_sound, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
if (_deleteOnFlash && !Owner.Deleted)
|
||||
{
|
||||
Owner.Delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ITimerTrigger.Trigger(TimerTriggerEventArgs eventArgs)
|
||||
{
|
||||
return Explode();
|
||||
}
|
||||
|
||||
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
|
||||
{
|
||||
Explode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
@@ -13,7 +14,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
public abstract class BaseCharger : Component
|
||||
{
|
||||
|
||||
public IEntity HeldItem { get; protected set; }
|
||||
protected IEntity _heldItem;
|
||||
protected ContainerSlot _container;
|
||||
protected PowerDeviceComponent _powerDevice;
|
||||
public CellChargerStatus Status => _status;
|
||||
@@ -58,37 +59,28 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will remove the item directly into the user's hand rather than the floor
|
||||
/// This will remove the item directly into the user's hand / floor
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
public void RemoveItemToHand(IEntity user)
|
||||
public void RemoveItem(IEntity user)
|
||||
{
|
||||
var heldItem = _container.ContainedEntity;
|
||||
if (heldItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
RemoveItem();
|
||||
|
||||
if (user.TryGetComponent(out HandsComponent handsComponent) &&
|
||||
heldItem.TryGetComponent(out ItemComponent itemComponent))
|
||||
_container.Remove(heldItem);
|
||||
if (user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will put the charger's item on the floor if available
|
||||
/// </summary>
|
||||
public void RemoveItem()
|
||||
{
|
||||
if (_container.ContainedEntity == null)
|
||||
{
|
||||
return;
|
||||
handsComponent.PutInHandOrDrop(heldItem.GetComponent<ItemComponent>());
|
||||
}
|
||||
|
||||
_container.Remove(HeldItem);
|
||||
HeldItem = null;
|
||||
if (heldItem.TryGetComponent(out ServerBatteryBarrelComponent batteryBarrelComponent))
|
||||
{
|
||||
batteryBarrelComponent.UpdateAppearance();
|
||||
}
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
@@ -135,8 +127,6 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(CellVisual.Occupied, _container.ContainedEntity != null);
|
||||
|
||||
_status = status;
|
||||
}
|
||||
|
||||
public void OnUpdate(float frameTime)
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
RemoveItemToHand(eventArgs.User);
|
||||
RemoveItem(eventArgs.User);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
@@ -111,7 +111,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
protected override void Activate(IEntity user, PowerCellChargerComponent component)
|
||||
{
|
||||
component.RemoveItem();
|
||||
component.RemoveItem(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +122,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HeldItem = entity;
|
||||
if (!_container.Insert(HeldItem))
|
||||
|
||||
if (!_container.Insert(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -157,7 +156,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
// Two numbers: One for how much power actually goes into the device (chargeAmount) and
|
||||
// chargeLoss which is how much is drawn from the powernet
|
||||
_container.ContainedEntity.TryGetComponent(out PowerCellComponent cellComponent);
|
||||
var cellComponent = _container.ContainedEntity.GetComponent<PowerCellComponent>();
|
||||
var chargeLoss = cellComponent.RequestCharge(frameTime) * _transferRatio;
|
||||
_powerDevice.Load = chargeLoss;
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
@@ -26,8 +21,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
public override string Name => "WeaponCapacitorCharger";
|
||||
public override double CellChargePercent => _container.ContainedEntity != null ?
|
||||
_container.ContainedEntity.GetComponent<HitscanWeaponCapacitorComponent>().Charge /
|
||||
_container.ContainedEntity.GetComponent<HitscanWeaponCapacitorComponent>().Capacity * 100 : 0.0f;
|
||||
_container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell.Charge /
|
||||
_container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell.Capacity * 100 : 0.0f;
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
@@ -43,7 +38,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
RemoveItemToHand(eventArgs.User);
|
||||
RemoveItem(eventArgs.User);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
@@ -106,21 +101,19 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
protected override void Activate(IEntity user, WeaponCapacitorChargerComponent component)
|
||||
{
|
||||
component.RemoveItem();
|
||||
component.RemoveItem(user);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryInsertItem(IEntity entity)
|
||||
{
|
||||
if (!entity.HasComponent<HitscanWeaponCapacitorComponent>() ||
|
||||
if (!entity.HasComponent<ServerBatteryBarrelComponent>() ||
|
||||
_container.ContainedEntity != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HeldItem = entity;
|
||||
|
||||
if (!_container.Insert(HeldItem))
|
||||
if (!_container.Insert(entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -140,8 +133,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
return CellChargerStatus.Empty;
|
||||
}
|
||||
|
||||
if (_container.ContainedEntity.TryGetComponent(out HitscanWeaponCapacitorComponent component) &&
|
||||
Math.Abs(component.Capacity - component.Charge) < 0.01)
|
||||
if (_container.ContainedEntity.TryGetComponent(out ServerBatteryBarrelComponent component) &&
|
||||
Math.Abs(component.PowerCell.Capacity - component.PowerCell.Charge) < 0.01)
|
||||
{
|
||||
return CellChargerStatus.Charged;
|
||||
}
|
||||
@@ -153,8 +146,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
{
|
||||
// Two numbers: One for how much power actually goes into the device (chargeAmount) and
|
||||
// chargeLoss which is how much is drawn from the powernet
|
||||
_container.ContainedEntity.TryGetComponent(out HitscanWeaponCapacitorComponent weaponCapacitorComponent);
|
||||
var chargeLoss = weaponCapacitorComponent.RequestCharge(frameTime) * _transferRatio;
|
||||
var powerCell = _container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell;
|
||||
var chargeLoss = powerCell.RequestCharge(frameTime) * _transferRatio;
|
||||
_powerDevice.Load = chargeLoss;
|
||||
|
||||
if (!_powerDevice.Powered)
|
||||
@@ -165,14 +158,13 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
|
||||
|
||||
var chargeAmount = chargeLoss * _transferEfficiency;
|
||||
|
||||
weaponCapacitorComponent.AddCharge(chargeAmount);
|
||||
powerCell.AddCharge(chargeAmount);
|
||||
// Just so the sprite won't be set to 99.99999% visibility
|
||||
if (weaponCapacitorComponent.Capacity - weaponCapacitorComponent.Charge < 0.01)
|
||||
if (powerCell.Capacity - powerCell.Charge < 0.01)
|
||||
{
|
||||
weaponCapacitorComponent.Charge = weaponCapacitorComponent.Capacity;
|
||||
powerCell.Charge = powerCell.Capacity;
|
||||
}
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Explosion;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ExplosiveProjectileComponent : Component, ICollideBehavior
|
||||
{
|
||||
public override string Name => "ExplosiveProjectile";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (!Owner.HasComponent<ExplosiveComponent>())
|
||||
{
|
||||
Logger.Error("ExplosiveProjectiles need an ExplosiveComponent");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
var explosiveComponent = Owner.GetComponent<ExplosiveComponent>();
|
||||
explosiveComponent.Explosion();
|
||||
}
|
||||
|
||||
// Projectile should handle the deleting
|
||||
void ICollideBehavior.PostCollide(int collisionCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon colliding with an object this will flash in an area around it
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class FlashProjectileComponent : Component, ICollideBehavior
|
||||
{
|
||||
public override string Name => "FlashProjectile";
|
||||
|
||||
private double _range;
|
||||
private double _duration;
|
||||
|
||||
private bool _flashed;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _range, "range", 1.0);
|
||||
serializer.DataField(ref _duration, "duration", 8.0);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
// Shouldn't be using this without a ProjectileComponent because it will just immediately collide with thrower
|
||||
if (!Owner.HasComponent<ProjectileComponent>())
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
if (_flashed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ServerFlashableComponent.FlashAreaHelper(Owner, _range, _duration);
|
||||
_flashed = true;
|
||||
}
|
||||
|
||||
// Projectile should handle the deleting
|
||||
void ICollideBehavior.PostCollide(int collisionCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Timer = Robust.Shared.Timers.Timer;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Lasers etc.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class HitscanComponent : Component
|
||||
{
|
||||
public override string Name => "Hitscan";
|
||||
public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask;
|
||||
private int _collisionMask;
|
||||
|
||||
public float Damage
|
||||
{
|
||||
get => _damage;
|
||||
set => _damage = value;
|
||||
}
|
||||
private float _damage;
|
||||
public DamageType DamageType => _damageType;
|
||||
private DamageType _damageType;
|
||||
public float MaxLength => 20.0f;
|
||||
|
||||
private TimeSpan _startTime;
|
||||
private TimeSpan _deathTime;
|
||||
|
||||
public float ColorModifier { get; set; } = 1.0f;
|
||||
private string _spriteName;
|
||||
private string _muzzleFlash;
|
||||
private string _impactFlash;
|
||||
private string _soundHitWall;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _collisionMask, "layers", (int) CollisionGroup.Opaque, WithFormat.Flags<CollisionLayer>());
|
||||
serializer.DataField(ref _damage, "damage", 10.0f);
|
||||
serializer.DataField(ref _damageType, "damageType", DamageType.Heat);
|
||||
serializer.DataField(ref _spriteName, "spriteName", "Objects/Guns/Projectiles/laser.png");
|
||||
serializer.DataField(ref _muzzleFlash, "muzzleFlash", null);
|
||||
serializer.DataField(ref _impactFlash, "impactFlash", null);
|
||||
serializer.DataField(ref _soundHitWall, "soundHitWall", "/Audio/Guns/Hits/laser_sear_wall.ogg");
|
||||
}
|
||||
|
||||
public void FireEffects(IEntity user, float distance, Angle angle, IEntity hitEntity = null)
|
||||
{
|
||||
var effectSystem = EntitySystem.Get<EffectSystem>();
|
||||
_startTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
_deathTime = _startTime + TimeSpan.FromSeconds(1);
|
||||
|
||||
var afterEffect = AfterEffects(user.Transform.GridPosition, angle, 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, angle);
|
||||
if (impactEffect != null)
|
||||
{
|
||||
effectSystem.CreateParticle(impactEffect);
|
||||
}
|
||||
|
||||
var muzzleEffect = MuzzleFlash(user.Transform.GridPosition, angle);
|
||||
if (muzzleEffect != null)
|
||||
{
|
||||
effectSystem.CreateParticle(muzzleEffect);
|
||||
}
|
||||
}
|
||||
|
||||
if (hitEntity != null && _soundHitWall != null)
|
||||
{
|
||||
// TODO: No wall component so ?
|
||||
var offset = angle.ToVec().Normalized / 2;
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundHitWall, user.Transform.GridPosition.Translated(offset));
|
||||
}
|
||||
|
||||
Timer.Spawn((int) _deathTime.TotalMilliseconds, () =>
|
||||
{
|
||||
if (!Owner.Deleted)
|
||||
{
|
||||
Owner.Delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private EffectSystemMessage MuzzleFlash(GridCoordinates 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.Translated(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(GridCoordinates 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.Translated(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 = Owner.Transform.GridPosition.Translated(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,13 @@
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -18,24 +22,32 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
|
||||
public bool IgnoreShooter = true;
|
||||
|
||||
private EntityUid Shooter = EntityUid.Invalid;
|
||||
private EntityUid _shooter = EntityUid.Invalid;
|
||||
|
||||
private Dictionary<DamageType, int> _damages;
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<DamageType, int> Damages => _damages;
|
||||
private float _velocity;
|
||||
public float Velocity
|
||||
public Dictionary<DamageType, int> Damages
|
||||
{
|
||||
get => _velocity;
|
||||
set => _velocity = value;
|
||||
get => _damages;
|
||||
set => _damages = value;
|
||||
}
|
||||
|
||||
public bool DeleteOnCollide => _deleteOnCollide;
|
||||
private bool _deleteOnCollide;
|
||||
|
||||
// Get that juicy FPS hit sound
|
||||
private string _soundHit;
|
||||
private string _soundHitSpecies;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _deleteOnCollide, "delete_on_collide", true);
|
||||
// If not specified 0 damage
|
||||
serializer.DataField(ref _damages, "damages", new Dictionary<DamageType, int>());
|
||||
serializer.DataField(ref _velocity, "velocity", 20f);
|
||||
serializer.DataField(ref _soundHit, "soundHit", null);
|
||||
serializer.DataField(ref _soundHitSpecies, "soundHitSpecies", null);
|
||||
}
|
||||
|
||||
public float TimeLeft { get; set; } = 10;
|
||||
@@ -46,7 +58,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
/// <param name="shooter"></param>
|
||||
public void IgnoreEntity(IEntity shooter)
|
||||
{
|
||||
Shooter = shooter.Uid;
|
||||
_shooter = shooter.Uid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,7 +68,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
/// <returns></returns>
|
||||
bool ICollideSpecial.PreventCollide(IPhysBody collidedwith)
|
||||
{
|
||||
if (IgnoreShooter && collidedwith.Owner.Uid == Shooter)
|
||||
if (IgnoreShooter && collidedwith.Owner.Uid == _shooter)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@@ -67,9 +79,17 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
/// <param name="entity"></param>
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
if (_soundHitSpecies != null && entity.HasComponent<SpeciesComponent>())
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundHitSpecies, entity.Transform.GridPosition);
|
||||
} else if (_soundHit != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundHit, entity.Transform.GridPosition);
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out DamageableComponent damage))
|
||||
{
|
||||
Owner.EntityManager.TryGetEntity(Shooter, out var shooter);
|
||||
Owner.EntityManager.TryGetEntity(_shooter, out var shooter);
|
||||
|
||||
foreach (var (damageType, amount) in _damages)
|
||||
{
|
||||
@@ -87,7 +107,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
|
||||
|
||||
void ICollideBehavior.PostCollide(int collideCount)
|
||||
{
|
||||
if (collideCount > 0) Owner.Delete();
|
||||
if (collideCount > 0 && DeleteOnCollide) Owner.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Projectiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds stun when it collides with an entity
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class StunnableProjectileComponent : Component, ICollideBehavior
|
||||
{
|
||||
public override string Name => "StunnableProjectile";
|
||||
|
||||
// See stunnable for what these do
|
||||
private int _stunAmount;
|
||||
private int _knockdownAmount;
|
||||
private int _slowdownAmount;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _stunAmount, "stunAmount", 0);
|
||||
serializer.DataField(ref _knockdownAmount, "knockdownAmount", 0);
|
||||
serializer.DataField(ref _slowdownAmount, "slowdownAmount", 0);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (!Owner.HasComponent<ProjectileComponent>())
|
||||
{
|
||||
Logger.Error("StunProjectile entity must have a ProjectileComponent");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.CollideWith(IEntity entity)
|
||||
{
|
||||
if (entity.TryGetComponent(out StunnableComponent stunnableComponent))
|
||||
{
|
||||
stunnableComponent.Stun(_stunAmount);
|
||||
stunnableComponent.Knockdown(_knockdownAmount);
|
||||
stunnableComponent.Slowdown(_slowdownAmount);
|
||||
}
|
||||
}
|
||||
|
||||
void ICollideBehavior.PostCollide(int collidedCount) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class AmmoBoxComponent : Component, IInteractUsing, IUse, IInteractHand, IMapInit
|
||||
{
|
||||
public override string Name => "AmmoBox";
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
public int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private Container _ammoContainer;
|
||||
private int _unspawnedCount;
|
||||
|
||||
private string _fillPrototype;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 30);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
_spawnedAmmo.Push(entity);
|
||||
_ammoContainer.Insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
_unspawnedCount += _capacity;
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
appearanceComponent.SetData(AmmoVisuals.AmmoCount, AmmoLeft);
|
||||
appearanceComponent.SetData(AmmoVisuals.AmmoMax, _capacity);
|
||||
}
|
||||
}
|
||||
|
||||
public IEntity TakeAmmo()
|
||||
{
|
||||
if (_spawnedAmmo.TryPop(out IEntity ammo))
|
||||
{
|
||||
_ammoContainer.Remove(ammo);
|
||||
return ammo;
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
ammo = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
|
||||
return ammo;
|
||||
}
|
||||
|
||||
public bool TryInsertAmmo(IEntity user, IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AmmoLeft >= Capacity)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("No room"));
|
||||
return false;
|
||||
}
|
||||
|
||||
_spawnedAmmo.Push(entity);
|
||||
_ammoContainer.Insert(entity);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Using.HasComponent<AmmoComponent>())
|
||||
{
|
||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
if (eventArgs.Using.TryGetComponent(out RangedMagazineComponent rangedMagazine))
|
||||
{
|
||||
for (var i = 0; i < Math.Max(10, rangedMagazine.ShotsLeft); i++)
|
||||
{
|
||||
var ammo = rangedMagazine.TakeAmmo();
|
||||
|
||||
if (!TryInsertAmmo(eventArgs.User, ammo))
|
||||
{
|
||||
rangedMagazine.TryInsertAmmo(eventArgs.User, ammo);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryUse(IEntity user)
|
||||
{
|
||||
if (!user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammo = TakeAmmo();
|
||||
var itemComponent = ammo.GetComponent<ItemComponent>();
|
||||
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
TryInsertAmmo(user, ammo);
|
||||
return false;
|
||||
}
|
||||
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EjectContents(int count)
|
||||
{
|
||||
var ejectCount = Math.Min(count, Capacity);
|
||||
var ejectAmmo = new List<IEntity>(ejectCount);
|
||||
|
||||
for (var i = 0; i < Math.Min(count, Capacity); i++)
|
||||
{
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
ejectAmmo.Add(ammo);
|
||||
}
|
||||
|
||||
ServerRangedBarrelComponent.EjectCasings(ejectAmmo);
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
return TryUse(eventArgs.User);
|
||||
}
|
||||
|
||||
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
|
||||
{
|
||||
return TryUse(eventArgs.User);
|
||||
}
|
||||
|
||||
// So if you have 200 rounds in a box and that suddenly creates 200 entities you're not having a fun time
|
||||
[Verb]
|
||||
private sealed class DumpVerb : Verb<AmmoBoxComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, AmmoBoxComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Dump 10");
|
||||
data.Visibility = component.AmmoLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, AmmoBoxComponent component)
|
||||
{
|
||||
component.EjectContents(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Timers;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Logger = Robust.Shared.Log.Logger;
|
||||
using Timer = Robust.Shared.Timers.Timer;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
/// <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]
|
||||
public class AmmoComponent : Component
|
||||
{
|
||||
public override string Name => "Ammo";
|
||||
public BallisticCaliber Caliber => _caliber;
|
||||
private BallisticCaliber _caliber;
|
||||
public bool Spent
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ammoIsProjectile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _spent;
|
||||
}
|
||||
}
|
||||
private bool _spent;
|
||||
|
||||
/// <summary>
|
||||
/// Used for anything without a case that fires itself
|
||||
/// </summary>
|
||||
private bool _ammoIsProjectile;
|
||||
|
||||
/// <summary>
|
||||
/// Used for something that is deleted when the projectile is retrieved
|
||||
/// </summary>
|
||||
public bool Caseless => _caseless;
|
||||
private bool _caseless;
|
||||
// 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>
|
||||
public int ProjectilesFired => _projectilesFired;
|
||||
private int _projectilesFired;
|
||||
private string _projectileId;
|
||||
// How far apart each entity is if multiple are shot
|
||||
public float EvenSpreadAngle => _evenSpreadAngle;
|
||||
private float _evenSpreadAngle;
|
||||
/// <summary>
|
||||
/// How fast the shot entities travel
|
||||
/// </summary>
|
||||
public float Velocity => _velocity;
|
||||
private float _velocity;
|
||||
|
||||
private string _muzzleFlashSprite;
|
||||
|
||||
public string SoundCollectionEject => _soundCollectionEject;
|
||||
private string _soundCollectionEject;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
// For shotty of whatever as well
|
||||
serializer.DataField(ref _projectileId, "projectile", null);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _projectilesFired, "projectilesFired", 1);
|
||||
// Used for shotty to determine overall pellet spread
|
||||
serializer.DataField(ref _evenSpreadAngle, "ammoSpread", 0);
|
||||
serializer.DataField(ref _velocity, "ammoVelocity", 20.0f);
|
||||
serializer.DataField(ref _ammoIsProjectile, "isProjectile", false);
|
||||
serializer.DataField(ref _caseless, "caseless", false);
|
||||
// Being both caseless and shooting yourself doesn't make sense
|
||||
DebugTools.Assert(!(_ammoIsProjectile && _caseless));
|
||||
serializer.DataField(ref _muzzleFlashSprite, "muzzleFlash", "Objects/Guns/Projectiles/bullet_muzzle.png");
|
||||
serializer.DataField(ref _soundCollectionEject, "soundCollectionEject", "CasingEject");
|
||||
|
||||
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 IEntity TakeBullet()
|
||||
{
|
||||
if (_ammoIsProjectile)
|
||||
{
|
||||
return Owner;
|
||||
}
|
||||
|
||||
if (_spent)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_spent = true;
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
appearanceComponent.SetData(AmmoVisuals.Spent, true);
|
||||
}
|
||||
|
||||
var entity = Owner.EntityManager.SpawnEntity(_projectileId, Owner.Transform.GridPosition);
|
||||
DebugTools.AssertNotNull(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public void MuzzleFlash(GridCoordinates grid, Angle angle)
|
||||
{
|
||||
if (_muzzleFlashSprite == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var time = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
var deathTime = time + TimeSpan.FromMilliseconds(200);
|
||||
// Offset the sprite so it actually looks like it's coming from the gun
|
||||
var offset = angle.ToVec().Normalized / 2;
|
||||
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = _muzzleFlashSprite,
|
||||
Born = time,
|
||||
DeathTime = deathTime,
|
||||
Coordinates = grid.Translated(offset),
|
||||
//Rotated from east facing
|
||||
Rotation = (float) angle.Theta,
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
Shaded = false
|
||||
};
|
||||
EntitySystem.Get<EffectSystem>().CreateParticle(message);
|
||||
}
|
||||
}
|
||||
|
||||
public enum BallisticCaliber
|
||||
{
|
||||
Unspecified = 0,
|
||||
A357, // Placeholder?
|
||||
ClRifle,
|
||||
SRifle,
|
||||
Pistol,
|
||||
A35, // Placeholder?
|
||||
LRifle,
|
||||
Magnum,
|
||||
AntiMaterial,
|
||||
Shotgun,
|
||||
Cap, // Placeholder
|
||||
Rocket,
|
||||
Dart, // Placeholder
|
||||
Grenade,
|
||||
Energy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class RangedMagazineComponent : Component, IMapInit, IInteractUsing, IUse
|
||||
{
|
||||
public override string Name => "RangedMagazine";
|
||||
|
||||
private Stack<IEntity> _spawnedAmmo = new Stack<IEntity>();
|
||||
private Container _ammoContainer;
|
||||
|
||||
public int ShotsLeft => _spawnedAmmo.Count + _unspawnedCount;
|
||||
public int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
public MagazineType MagazineType => _magazineType;
|
||||
private MagazineType _magazineType;
|
||||
public BallisticCaliber Caliber => _caliber;
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// If there's anything already in the magazine
|
||||
private 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
|
||||
private int _unspawnedCount;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _magazineType, "magazineType", MagazineType.Unspecified);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
serializer.DataField(ref _capacity, "capacity", 20);
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_fillPrototype != null)
|
||||
{
|
||||
_unspawnedCount += Capacity;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-magazine", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
if (_ammoContainer.ContainedEntities.Count > Capacity)
|
||||
{
|
||||
throw new InvalidOperationException("Initialized capacity of magazine higher than its actual capacity");
|
||||
}
|
||||
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_spawnedAmmo.Push(entity);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public bool TryInsertAmmo(IEntity user, IEntity ammo)
|
||||
{
|
||||
if (!ammo.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ShotsLeft >= Capacity)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Magazine is full"));
|
||||
return false;
|
||||
}
|
||||
|
||||
_ammoContainer.Insert(ammo);
|
||||
_spawnedAmmo.Push(ammo);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEntity TakeAmmo()
|
||||
{
|
||||
IEntity ammo = null;
|
||||
// If anything's spawned use that first, otherwise use the fill prototype as a fallback (if we have spawn count left)
|
||||
if (_spawnedAmmo.TryPop(out var entity))
|
||||
{
|
||||
ammo = entity;
|
||||
_ammoContainer.Remove(entity);
|
||||
}
|
||||
else if (_unspawnedCount > 0)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
ammo = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
return ammo;
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.User.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemComponent = ammo.GetComponent<ItemComponent>();
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
ammo.Transform.GridPosition = eventArgs.User.Transform.GridPosition;
|
||||
ServerRangedBarrelComponent.EjectCasing(ammo);
|
||||
}
|
||||
else
|
||||
{
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to load certain ranged weapons quickly
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class SpeedLoaderComponent : Component, IAfterInteract, IInteractUsing, IMapInit, IUse
|
||||
{
|
||||
public override string Name => "SpeedLoader";
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
public int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
private Container _ammoContainer;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private int _unspawnedCount;
|
||||
|
||||
public int AmmoLeft => _spawnedAmmo.Count + _unspawnedCount;
|
||||
|
||||
private string _fillPrototype;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 6);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var ammo in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
_spawnedAmmo.Push(ammo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
_unspawnedCount += _capacity;
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
appearanceComponent?.SetData(AmmoVisuals.AmmoCount, AmmoLeft);
|
||||
appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryInsertAmmo(IEntity user, IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AmmoLeft >= Capacity)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("No room"));
|
||||
return false;
|
||||
}
|
||||
|
||||
_spawnedAmmo.Push(entity);
|
||||
_ammoContainer.Insert(entity);
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private bool UseEntity(IEntity user)
|
||||
{
|
||||
if (!user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemComponent = ammo.GetComponent<ItemComponent>();
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
ServerRangedBarrelComponent.EjectCasing(ammo);
|
||||
}
|
||||
else
|
||||
{
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEntity TakeAmmo()
|
||||
{
|
||||
if (_spawnedAmmo.TryPop(out var entity))
|
||||
{
|
||||
_ammoContainer.Remove(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
entity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
|
||||
bool changed = false;
|
||||
|
||||
if (eventArgs.Target.TryGetComponent(out RevolverBarrelComponent revolverBarrel))
|
||||
{
|
||||
for (var i = 0; i < Capacity; i++)
|
||||
{
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (revolverBarrel.TryInsertBullet(eventArgs.User, ammo))
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the ammo back
|
||||
TryInsertAmmo(eventArgs.User, ammo);
|
||||
break;
|
||||
}
|
||||
} else if (eventArgs.Target.TryGetComponent(out BoltActionBarrelComponent boltActionBarrel))
|
||||
{
|
||||
for (var i = 0; i < Capacity; i++)
|
||||
{
|
||||
var ammo = TakeAmmo();
|
||||
if (ammo == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (boltActionBarrel.TryInsertBullet(eventArgs.User, ammo))
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the ammo back
|
||||
TryInsertAmmo(eventArgs.User, ammo);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
UpdateAppearance();
|
||||
}
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertAmmo(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
return UseEntity(eventArgs.User);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
/// <summary>
|
||||
/// Shotguns mostly
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent, IMapInit
|
||||
{
|
||||
// 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 string Name => "BoltActionBarrel";
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
|
||||
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
|
||||
}
|
||||
}
|
||||
public override int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
private ContainerSlot _chamberContainer;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private Container _ammoContainer;
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
private string _fillPrototype;
|
||||
private int _unspawnedCount;
|
||||
|
||||
public bool BoltOpen
|
||||
{
|
||||
get => _boltOpen;
|
||||
set
|
||||
{
|
||||
if (_boltOpen == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
|
||||
if (value)
|
||||
{
|
||||
if (_soundBoltOpen != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_soundBoltClosed != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
_boltOpen = value;
|
||||
UpdateAppearance();
|
||||
}
|
||||
}
|
||||
private bool _boltOpen;
|
||||
private bool _autoCycle;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
// Sounds
|
||||
private string _soundCycle;
|
||||
private string _soundBoltOpen;
|
||||
private string _soundBoltClosed;
|
||||
private string _soundInsert;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 6);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
serializer.DataField(ref _autoCycle, "autoCycle", false);
|
||||
|
||||
serializer.DataField(ref _soundCycle, "soundCycle", "/Audio/Guns/Cock/sf_rifle_cock.ogg");
|
||||
serializer.DataField(ref _soundBoltOpen, "soundBoltOpen", "/Audio/Guns/Bolt/rifle_bolt_open.ogg");
|
||||
serializer.DataField(ref _soundBoltClosed, "soundBoltClosed", "/Audio/Guns/Bolt/rifle_bolt_closed.ogg");
|
||||
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/bullet_insert.ogg");
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_fillPrototype != null)
|
||||
{
|
||||
_unspawnedCount += Capacity - 1;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
// TODO: Add existing ammo support on revolvers
|
||||
base.Initialize();
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity - 1);
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-ammo-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_spawnedAmmo.Push(entity);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
_chamberContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber-container", Owner);
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(BarrelBoltVisuals.BoltOpen, BoltOpen);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return _chamberContainer.ContainedEntity;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var chamberEntity = _chamberContainer.ContainedEntity;
|
||||
if (_autoCycle)
|
||||
{
|
||||
Cycle();
|
||||
}
|
||||
return chamberEntity?.GetComponent<AmmoComponent>().TakeBullet();
|
||||
}
|
||||
|
||||
protected override bool WeaponCanFire()
|
||||
{
|
||||
if (!base.WeaponCanFire())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !BoltOpen && _chamberContainer.ContainedEntity != null;
|
||||
}
|
||||
|
||||
private void Cycle(bool manual = false)
|
||||
{
|
||||
var chamberedEntity = _chamberContainer.ContainedEntity;
|
||||
if (chamberedEntity != null)
|
||||
{
|
||||
_chamberContainer.Remove(chamberedEntity);
|
||||
var ammoComponent = chamberedEntity.GetComponent<AmmoComponent>();
|
||||
if (!ammoComponent.Caseless)
|
||||
{
|
||||
EjectCasing(chamberedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (_spawnedAmmo.TryPop(out var next))
|
||||
{
|
||||
_ammoContainer.Remove(next);
|
||||
_chamberContainer.Insert(next);
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
var ammoEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_chamberContainer.Insert(ammoEntity);
|
||||
}
|
||||
|
||||
if (_chamberContainer.ContainedEntity == null && manual)
|
||||
{
|
||||
BoltOpen = true;
|
||||
if (ContainerHelpers.TryGetContainer(Owner, out var container))
|
||||
{
|
||||
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt opened"));
|
||||
}
|
||||
}
|
||||
|
||||
if (manual)
|
||||
{
|
||||
if (_soundCycle != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundCycle, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public bool TryInsertBullet(IEntity user, IEntity ammo)
|
||||
{
|
||||
if (!ammo.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Bolt isn't open"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_chamberContainer.ContainedEntity == null)
|
||||
{
|
||||
_chamberContainer.Insert(ammo);
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
|
||||
{
|
||||
_ammoContainer.Insert(ammo);
|
||||
_spawnedAmmo.Push(ammo);
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user, Loc.GetString("No room"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (BoltOpen)
|
||||
{
|
||||
BoltOpen = false;
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Bolt closed"));
|
||||
// Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
Cycle(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertBullet(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class OpenBoltVerb : Verb<BoltActionBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BoltActionBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Open bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Disabled : VerbVisibility.Visible;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BoltActionBarrelComponent component)
|
||||
{
|
||||
component.BoltOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class CloseBoltVerb : Verb<BoltActionBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BoltActionBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Close bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BoltActionBarrelComponent component)
|
||||
{
|
||||
component.BoltOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
/// <summary>
|
||||
/// Bolt-action rifles
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, IMapInit
|
||||
{
|
||||
public override string Name => "PumpBarrel";
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var chamberCount = _chamberContainer.ContainedEntity != null ? 1 : 0;
|
||||
return chamberCount + _spawnedAmmo.Count + _unspawnedCount;
|
||||
}
|
||||
}
|
||||
|
||||
public override int Capacity => _capacity;
|
||||
private int _capacity;
|
||||
|
||||
// Even a point having a chamber? I guess it makes some of the below code cleaner
|
||||
private ContainerSlot _chamberContainer;
|
||||
private Stack<IEntity> _spawnedAmmo;
|
||||
private Container _ammoContainer;
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
private string _fillPrototype;
|
||||
private int _unspawnedCount;
|
||||
|
||||
private bool _manualCycle;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundCycle;
|
||||
private string _soundInsert;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _capacity, "capacity", 6);
|
||||
serializer.DataField(ref _fillPrototype, "fillPrototype", null);
|
||||
serializer.DataField(ref _manualCycle, "manualCycle", true);
|
||||
|
||||
serializer.DataField(ref _soundCycle, "soundCycle", "/Audio/Guns/Cock/sf_rifle_cock.ogg");
|
||||
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/bullet_insert.ogg");
|
||||
|
||||
_spawnedAmmo = new Stack<IEntity>(_capacity - 1);
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_fillPrototype != null)
|
||||
{
|
||||
_unspawnedCount += Capacity - 1;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_ammoContainer =
|
||||
ContainerManagerComponent.Ensure<Container>($"{Name}-ammo-container", Owner, out var existing);
|
||||
|
||||
if (existing)
|
||||
{
|
||||
foreach (var entity in _ammoContainer.ContainedEntities)
|
||||
{
|
||||
_spawnedAmmo.Push(entity);
|
||||
_unspawnedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
_chamberContainer =
|
||||
ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber-container", Owner, out existing);
|
||||
if (existing)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return _chamberContainer.ContainedEntity;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var chamberEntity = _chamberContainer.ContainedEntity;
|
||||
if (!_manualCycle)
|
||||
{
|
||||
Cycle();
|
||||
}
|
||||
return chamberEntity?.GetComponent<AmmoComponent>().TakeBullet();
|
||||
}
|
||||
|
||||
private void Cycle(bool manual = false)
|
||||
{
|
||||
var chamberedEntity = _chamberContainer.ContainedEntity;
|
||||
if (chamberedEntity != null)
|
||||
{
|
||||
_chamberContainer.Remove(chamberedEntity);
|
||||
var ammoComponent = chamberedEntity.GetComponent<AmmoComponent>();
|
||||
if (!ammoComponent.Caseless)
|
||||
{
|
||||
EjectCasing(chamberedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (_spawnedAmmo.TryPop(out var next))
|
||||
{
|
||||
_ammoContainer.Remove(next);
|
||||
_chamberContainer.Insert(next);
|
||||
}
|
||||
|
||||
if (_unspawnedCount > 0)
|
||||
{
|
||||
_unspawnedCount--;
|
||||
var ammoEntity = Owner.EntityManager.SpawnEntity(_fillPrototype, Owner.Transform.GridPosition);
|
||||
_chamberContainer.Insert(ammoEntity);
|
||||
}
|
||||
|
||||
if (manual)
|
||||
{
|
||||
if (_soundCycle != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundCycle, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public bool TryInsertBullet(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ammoContainer.ContainedEntities.Count < Capacity - 1)
|
||||
{
|
||||
_ammoContainer.Insert(eventArgs.Using);
|
||||
_spawnedAmmo.Push(eventArgs.Using);
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("No room"));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
Cycle(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertBullet(eventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent
|
||||
{
|
||||
public override string Name => "RevolverBarrel";
|
||||
private BallisticCaliber _caliber;
|
||||
private Container _ammoContainer;
|
||||
private int _currentSlot = 0;
|
||||
public override int Capacity => _ammoSlots.Length;
|
||||
private IEntity[] _ammoSlots;
|
||||
|
||||
public override int ShotsLeft => _ammoContainer.ContainedEntities.Count;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundEject;
|
||||
private string _soundInsert;
|
||||
private string _soundSpin;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
var capacity = serializer.ReadDataField("capacity", 6);
|
||||
_ammoSlots = new IEntity[capacity];
|
||||
|
||||
// Sounds
|
||||
serializer.DataField(ref _soundEject, "soundEject", "/Audio/Guns/MagOut/revolver_magout.ogg");
|
||||
serializer.DataField(ref _soundInsert, "soundInsert", "/Audio/Guns/MagIn/revolver_magin.ogg");
|
||||
serializer.DataField(ref _soundSpin, "soundSpin", "/Audio/Guns/Misc/revolver_spin.ogg");
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<Container>($"{Name}-ammoContainer", Owner);
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, true);
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
// Placeholder, at this stage it's just here for the RPG
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, ShotsLeft > 0);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public bool TryInsertBullet(IEntity user, IEntity entity)
|
||||
{
|
||||
if (!entity.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Wrong caliber"));
|
||||
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 = _ammoSlots.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var slot = _ammoSlots[i];
|
||||
if (slot == null)
|
||||
{
|
||||
_currentSlot = i;
|
||||
_ammoSlots[i] = entity;
|
||||
_ammoContainer.Insert(entity);
|
||||
if (_soundInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user, Loc.GetString("Ammo full"));
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Cycle()
|
||||
{
|
||||
// Move up a slot
|
||||
_currentSlot = (_currentSlot + 1) % _ammoSlots.Length;
|
||||
// Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Russian Roulette
|
||||
/// </summary>
|
||||
public void Spin()
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>().Next(_ammoSlots.Length - 1);
|
||||
_currentSlot = random;
|
||||
if (_soundSpin != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundSpin, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return _ammoSlots[_currentSlot];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a projectile out if possible
|
||||
/// IEnumerable just to make supporting shotguns saner
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var ammo = _ammoSlots[_currentSlot];
|
||||
IEntity bullet = null;
|
||||
if (ammo != null)
|
||||
{
|
||||
var ammoComponent = ammo.GetComponent<AmmoComponent>();
|
||||
bullet = ammoComponent.TakeBullet();
|
||||
if (ammoComponent.Caseless)
|
||||
{
|
||||
_ammoSlots[_currentSlot] = null;
|
||||
_ammoContainer.Remove(ammo);
|
||||
}
|
||||
}
|
||||
Cycle();
|
||||
UpdateAppearance();
|
||||
return bullet;
|
||||
}
|
||||
|
||||
private void EjectAllSlots()
|
||||
{
|
||||
for (var i = 0; i < _ammoSlots.Length; i++)
|
||||
{
|
||||
var entity = _ammoSlots[i];
|
||||
if (entity == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_ammoContainer.Remove(entity);
|
||||
EjectCasing(entity);
|
||||
_ammoSlots[i] = null;
|
||||
}
|
||||
|
||||
if (_ammoContainer.ContainedEntities.Count > 0)
|
||||
{
|
||||
if (_soundEject != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
|
||||
}
|
||||
}
|
||||
|
||||
// May as well point back at the end?
|
||||
_currentSlot = _ammoSlots.Length - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eject all casings
|
||||
/// </summary>
|
||||
/// <param name="eventArgs"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
EjectAllSlots();
|
||||
//Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return TryInsertBullet(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class SpinRevolverVerb : Verb<RevolverBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, RevolverBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Spin");
|
||||
if (component.Capacity <= 1)
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Visibility = component.ShotsLeft > 0 ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, RevolverBarrelComponent component)
|
||||
{
|
||||
component.Spin();
|
||||
component.Owner.PopupMessage(user, Loc.GetString("Spun the cylinder"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Server.GameObjects.Components.Projectiles;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Logger = Robust.Shared.Log.Logger;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ServerBatteryBarrelComponent : ServerRangedBarrelComponent
|
||||
{
|
||||
public override string Name => "BatteryBarrel";
|
||||
|
||||
// The minimum change we need before we can fire
|
||||
[ViewVariables] private float _lowerChargeLimit;
|
||||
[ViewVariables] private int _baseFireCost;
|
||||
// What gets fired
|
||||
[ViewVariables] private string _ammoPrototype;
|
||||
|
||||
[ViewVariables] public IEntity PowerCellEntity => _powerCellContainer.ContainedEntity;
|
||||
public PowerCellComponent PowerCell => _powerCellContainer.ContainedEntity.GetComponent<PowerCellComponent>();
|
||||
private ContainerSlot _powerCellContainer;
|
||||
private ContainerSlot _ammoContainer;
|
||||
private string _powerCellPrototype;
|
||||
[ViewVariables] private bool _powerCellRemovable;
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var powerCell = _powerCellContainer.ContainedEntity;
|
||||
|
||||
if (powerCell == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) Math.Ceiling(powerCell.GetComponent<PowerCellComponent>().Charge / _baseFireCost);
|
||||
}
|
||||
}
|
||||
|
||||
public override int Capacity
|
||||
{
|
||||
get
|
||||
{
|
||||
var powerCell = _powerCellContainer.ContainedEntity;
|
||||
|
||||
if (powerCell == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) Math.Ceiling(powerCell.GetComponent<PowerCellComponent>().Capacity / _baseFireCost);
|
||||
}
|
||||
}
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundPowerCellInsert;
|
||||
private string _soundPowerCellEject;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
if (serializer.Reading)
|
||||
{
|
||||
_powerCellPrototype = serializer.ReadDataField<string>("powerCellPrototype", null);
|
||||
}
|
||||
|
||||
serializer.DataField(ref _powerCellRemovable, "powerCellRemovable", false);
|
||||
serializer.DataField(ref _baseFireCost, "fireCost", 300);
|
||||
serializer.DataField(ref _ammoPrototype, "ammoPrototype", null);
|
||||
serializer.DataField(ref _lowerChargeLimit, "lowerChargeLimit", 10);
|
||||
serializer.DataField(ref _soundPowerCellInsert, "soundPowerCellInsert", null);
|
||||
serializer.DataField(ref _soundPowerCellEject, "soundPowerCellEject", null);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_powerCellContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-powercell-container", Owner, out var existing);
|
||||
if (!existing && _powerCellPrototype != null)
|
||||
{
|
||||
var powerCellEntity = Owner.EntityManager.SpawnEntity(_powerCellPrototype, Owner.Transform.GridPosition);
|
||||
_powerCellContainer.Insert(powerCellEntity);
|
||||
}
|
||||
|
||||
if (_ammoPrototype != null)
|
||||
{
|
||||
_ammoContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-ammo-container", Owner);
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _powerCellContainer.ContainedEntity != null);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
// Spawn a dummy entity because it's easier to work with I guess
|
||||
// This will get re-used for the projectile
|
||||
var ammo = _ammoContainer.ContainedEntity;
|
||||
if (ammo == null)
|
||||
{
|
||||
ammo = Owner.EntityManager.SpawnEntity(_ammoPrototype, Owner.Transform.GridPosition);
|
||||
_ammoContainer.Insert(ammo);
|
||||
}
|
||||
|
||||
return ammo;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
var powerCellEntity = _powerCellContainer.ContainedEntity;
|
||||
|
||||
if (powerCellEntity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var capacitor = powerCellEntity.GetComponent<PowerCellComponent>();
|
||||
if (capacitor.Charge < _lowerChargeLimit)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Can fire confirmed
|
||||
// Multiply the entity's damage / whatever by the percentage of charge the shot has.
|
||||
IEntity entity;
|
||||
var chargeChange = Math.Min(capacitor.Charge, _baseFireCost);
|
||||
capacitor.DeductCharge(chargeChange);
|
||||
var energyRatio = chargeChange / _baseFireCost;
|
||||
|
||||
if (_ammoContainer.ContainedEntity != null)
|
||||
{
|
||||
entity = _ammoContainer.ContainedEntity;
|
||||
_ammoContainer.Remove(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = Owner.EntityManager.SpawnEntity(_ammoPrototype, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out ProjectileComponent projectileComponent))
|
||||
{
|
||||
if (energyRatio < 1.0)
|
||||
{
|
||||
var newDamages = new Dictionary<DamageType, int>(projectileComponent.Damages);
|
||||
foreach (var (damageType, damage) in projectileComponent.Damages)
|
||||
{
|
||||
newDamages.Add(damageType, (int) (damage * energyRatio));
|
||||
}
|
||||
|
||||
projectileComponent.Damages = newDamages;
|
||||
}
|
||||
} else if (entity.TryGetComponent(out HitscanComponent hitscanComponent))
|
||||
{
|
||||
hitscanComponent.Damage *= energyRatio;
|
||||
hitscanComponent.ColorModifier = energyRatio;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
//Dirty();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public bool TryInsertPowerCell(IEntity entity)
|
||||
{
|
||||
if (_powerCellContainer.ContainedEntity != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!entity.HasComponent<PowerCellComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_soundPowerCellInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundPowerCellInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
_powerCellContainer.Insert(entity);
|
||||
UpdateAppearance();
|
||||
//Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEntity RemovePowerCell()
|
||||
{
|
||||
if (!_powerCellRemovable || _powerCellContainer.ContainedEntity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _powerCellContainer.ContainedEntity;
|
||||
_powerCellContainer.Remove(entity);
|
||||
if (_soundPowerCellEject != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundPowerCellEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
//Dirty();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (!_powerCellRemovable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!eventArgs.User.TryGetComponent(out HandsComponent handsComponent) ||
|
||||
PowerCellEntity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemComponent = PowerCellEntity.GetComponent<ItemComponent>();
|
||||
if (!handsComponent.CanPutInHand(itemComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var powerCell = RemovePowerCell();
|
||||
handsComponent.PutInHand(itemComponent);
|
||||
powerCell.Transform.GridPosition = eventArgs.User.Transform.GridPosition;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.HasComponent<PowerStorageComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryInsertPowerCell(eventArgs.Using);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ServerMagazineBarrelComponent : ServerRangedBarrelComponent
|
||||
{
|
||||
public override string Name => "MagazineBarrel";
|
||||
public override uint? NetID => ContentNetIDs.MAGAZINE_BARREL;
|
||||
|
||||
private ContainerSlot _chamberContainer;
|
||||
[ViewVariables] public bool HasMagazine => _magazineContainer.ContainedEntity != null;
|
||||
private ContainerSlot _magazineContainer;
|
||||
|
||||
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
|
||||
private MagazineType _magazineTypes;
|
||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
||||
private BallisticCaliber _caliber;
|
||||
|
||||
public override int ShotsLeft
|
||||
{
|
||||
get
|
||||
{
|
||||
var count = 0;
|
||||
if (_chamberContainer.ContainedEntity != null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
if (magazine != null)
|
||||
{
|
||||
count += magazine.GetComponent<RangedMagazineComponent>().ShotsLeft;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public override int Capacity
|
||||
{
|
||||
get
|
||||
{
|
||||
// Chamber
|
||||
var count = 1;
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
if (magazine != null)
|
||||
{
|
||||
count += magazine.GetComponent<RangedMagazineComponent>().Capacity;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
public bool BoltOpen { get; private set; } = true;
|
||||
private bool _autoEjectMag;
|
||||
// If the bolt needs to be open before we can insert / remove the mag (i.e. for LMGs)
|
||||
public bool MagNeedsOpenBolt => _magNeedsOpenBolt;
|
||||
private bool _magNeedsOpenBolt;
|
||||
|
||||
private AppearanceComponent _appearanceComponent;
|
||||
|
||||
// Sounds
|
||||
private string _soundBoltOpen;
|
||||
private string _soundBoltClosed;
|
||||
private string _soundRack;
|
||||
private string _soundMagInsert;
|
||||
private string _soundMagEject;
|
||||
private string _soundAutoEject;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
if (serializer.Reading)
|
||||
{
|
||||
var magTypes = serializer.ReadDataField("magazineTypes", new List<MagazineType>());
|
||||
foreach (var mag in magTypes)
|
||||
{
|
||||
_magazineTypes |= mag;
|
||||
}
|
||||
}
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _autoEjectMag, "autoEjectMag", false);
|
||||
serializer.DataField(ref _magNeedsOpenBolt, "magNeedsOpenBolt", false);
|
||||
serializer.DataField(ref _soundBoltOpen, "soundBoltOpen", null);
|
||||
serializer.DataField(ref _soundBoltClosed, "soundBoltClosed", null);
|
||||
serializer.DataField(ref _soundRack, "soundRack", null);
|
||||
serializer.DataField(ref _soundMagInsert, "soundMagInsert", null);
|
||||
serializer.DataField(ref _soundMagEject, "soundMagEject", null);
|
||||
serializer.DataField(ref _soundAutoEject, "soundAutoEject", "/Audio/Guns/EmptyAlarm/smg_empty_alarm.ogg");
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
(int, int)? count = null;
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
if (magazine != null && magazine.TryGetComponent(out RangedMagazineComponent rangedMagazineComponent))
|
||||
{
|
||||
count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
|
||||
}
|
||||
|
||||
return new MagazineBarrelComponentState(
|
||||
_chamberContainer.ContainedEntity != null,
|
||||
FireRateSelector,
|
||||
count,
|
||||
SoundGunshot);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (Owner.TryGetComponent(out AppearanceComponent appearanceComponent))
|
||||
{
|
||||
_appearanceComponent = appearanceComponent;
|
||||
}
|
||||
|
||||
_chamberContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-chamber", Owner);
|
||||
_magazineContainer = ContainerManagerComponent.Ensure<ContainerSlot>($"{Name}-magazine", Owner);
|
||||
}
|
||||
|
||||
public void ToggleBolt()
|
||||
{
|
||||
// For magazines only when we normally set BoltOpen we'll defer the UpdateAppearance until everything is done
|
||||
// Whereas this will just call it straight up.
|
||||
BoltOpen = !BoltOpen;
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
if (BoltOpen)
|
||||
{
|
||||
if (_soundBoltOpen != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_soundBoltClosed != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
}
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override IEntity PeekAmmo()
|
||||
{
|
||||
return BoltOpen ? null : _chamberContainer.ContainedEntity;
|
||||
}
|
||||
|
||||
public override IEntity TakeProjectile()
|
||||
{
|
||||
if (BoltOpen)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var entity = _chamberContainer.ContainedEntity;
|
||||
|
||||
Cycle();
|
||||
return entity?.GetComponent<AmmoComponent>().TakeBullet();
|
||||
}
|
||||
|
||||
private void Cycle(bool manual = false)
|
||||
{
|
||||
if (BoltOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var chamberEntity = _chamberContainer.ContainedEntity;
|
||||
if (chamberEntity != null)
|
||||
{
|
||||
_chamberContainer.Remove(chamberEntity);
|
||||
var ammoComponent = chamberEntity.GetComponent<AmmoComponent>();
|
||||
if (!ammoComponent.Caseless)
|
||||
{
|
||||
EjectCasing(chamberEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// Try and pull a round from the magazine to replace the chamber if possible
|
||||
var magazine = _magazineContainer.ContainedEntity;
|
||||
var nextRound = magazine?.GetComponent<RangedMagazineComponent>().TakeAmmo();
|
||||
|
||||
if (nextRound != null)
|
||||
{
|
||||
// If you're really into gunporn you could put a sound here
|
||||
_chamberContainer.Insert(nextRound);
|
||||
}
|
||||
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
|
||||
if (_autoEjectMag && magazine != null && magazine.GetComponent<RangedMagazineComponent>().ShotsLeft == 0)
|
||||
{
|
||||
if (_soundAutoEject != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundAutoEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
_magazineContainer.Remove(magazine);
|
||||
}
|
||||
|
||||
if (nextRound == null && !BoltOpen)
|
||||
{
|
||||
if (_soundBoltOpen != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundBoltOpen, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
|
||||
if (ContainerHelpers.TryGetContainer(Owner, out var container))
|
||||
{
|
||||
Owner.PopupMessage(container.Owner, Loc.GetString("Bolt open"));
|
||||
}
|
||||
BoltOpen = true;
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return;
|
||||
}
|
||||
|
||||
if (manual)
|
||||
{
|
||||
if (_soundRack != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundRack, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearanceComponent?.SetData(BarrelBoltVisuals.BoltOpen, BoltOpen);
|
||||
_appearanceComponent?.SetData(MagazineBarrelVisuals.MagLoaded, _magazineContainer.ContainedEntity != null);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoCount, ShotsLeft);
|
||||
_appearanceComponent?.SetData(AmmoVisuals.AmmoMax, Capacity);
|
||||
}
|
||||
|
||||
public override bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
// 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
|
||||
|
||||
if (BoltOpen)
|
||||
{
|
||||
if (_soundBoltClosed != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundBoltClosed, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Bolt closed"));
|
||||
BoltOpen = false;
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Could play a rack-slide specific sound here if you're so inclined (if the chamber is empty but rounds are available)
|
||||
|
||||
Cycle(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveMagazine(IEntity user)
|
||||
{
|
||||
var mag = _magazineContainer.ContainedEntity;
|
||||
|
||||
if (mag == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (MagNeedsOpenBolt && !BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("Bolt needs to be open"));
|
||||
return;
|
||||
}
|
||||
|
||||
_magazineContainer.Remove(mag);
|
||||
if (_soundMagEject != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundMagEject, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
|
||||
if (user.TryGetComponent(out HandsComponent handsComponent))
|
||||
{
|
||||
handsComponent.PutInHandOrDrop(mag.GetComponent<ItemComponent>());
|
||||
}
|
||||
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
// Insert magazine
|
||||
if (eventArgs.Using.TryGetComponent(out RangedMagazineComponent magazineComponent))
|
||||
{
|
||||
if ((MagazineTypes & magazineComponent.MagazineType) == 0)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong magazine type"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (magazineComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_magNeedsOpenBolt && !BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Need to open bolt first"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_magazineContainer.ContainedEntity == null)
|
||||
{
|
||||
if (_soundMagInsert != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(_soundMagInsert, Owner.Transform.GridPosition, AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Magazine inserted"));
|
||||
_magazineContainer.Insert(eventArgs.Using);
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Already holding a magazine"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Insert 1 ammo
|
||||
if (eventArgs.Using.TryGetComponent(out AmmoComponent ammoComponent))
|
||||
{
|
||||
if (!BoltOpen)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Cannot insert ammo while bolt is closed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ammoComponent.Caliber != _caliber)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Wrong caliber"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_chamberContainer.ContainedEntity == null)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Ammo inserted"));
|
||||
_chamberContainer.Insert(eventArgs.Using);
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
return true;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("Chamber full"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class EjectMagazineVerb : Verb<ServerMagazineBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Eject magazine");
|
||||
if (component.MagNeedsOpenBolt)
|
||||
{
|
||||
data.Visibility = component.HasMagazine && component.BoltOpen
|
||||
? VerbVisibility.Visible
|
||||
: VerbVisibility.Disabled;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Visibility = component.HasMagazine ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
|
||||
{
|
||||
component.RemoveMagazine(user);
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class OpenBoltVerb : Verb<ServerMagazineBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Open bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Disabled : VerbVisibility.Visible;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
|
||||
{
|
||||
component.ToggleBolt();
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
private sealed class CloseBoltVerb : Verb<ServerMagazineBarrelComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ServerMagazineBarrelComponent component, VerbData data)
|
||||
{
|
||||
data.Text = Loc.GetString("Close bolt");
|
||||
data.Visibility = component.BoltOpen ? VerbVisibility.Visible : VerbVisibility.Disabled;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ServerMagazineBarrelComponent component)
|
||||
{
|
||||
component.ToggleBolt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Projectiles;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Physics;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
||||
{
|
||||
/// <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>
|
||||
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IUse, IInteractUsing
|
||||
{
|
||||
// There's still some of py01 and PJB's work left over, especially in underlying shooting logic,
|
||||
// it's just when I re-organised it changed me as the contributor
|
||||
#pragma warning disable 649
|
||||
[Dependency] private IGameTiming _gameTiming;
|
||||
[Dependency] private IRobustRandom _robustRandom;
|
||||
#pragma warning restore 649
|
||||
|
||||
public override FireRateSelector FireRateSelector => _fireRateSelector;
|
||||
private FireRateSelector _fireRateSelector;
|
||||
public override FireRateSelector AllRateSelectors => _fireRateSelector;
|
||||
private FireRateSelector _allRateSelectors;
|
||||
public override float FireRate => _fireRate;
|
||||
private float _fireRate;
|
||||
|
||||
// _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
|
||||
private TimeSpan _lastFire;
|
||||
|
||||
public abstract IEntity PeekAmmo();
|
||||
public abstract IEntity TakeProjectile();
|
||||
|
||||
// Recoil / spray control
|
||||
private Angle _minAngle;
|
||||
private Angle _maxAngle;
|
||||
private Angle _currentAngle = Angle.Zero;
|
||||
/// <summary>
|
||||
/// How slowly the angle's theta decays per second in radians
|
||||
/// </summary>
|
||||
private float _angleDecay;
|
||||
/// <summary>
|
||||
/// How quickly the angle's theta builds for every shot fired in radians
|
||||
/// </summary>
|
||||
private float _angleIncrease;
|
||||
// Multiplies the ammo spread to get the final spread of each pellet
|
||||
private float _spreadRatio;
|
||||
|
||||
public bool CanMuzzleFlash => _canMuzzleFlash;
|
||||
private bool _canMuzzleFlash = true;
|
||||
|
||||
// Sounds
|
||||
public string SoundGunshot
|
||||
{
|
||||
get => _soundGunshot;
|
||||
set => _soundGunshot = value;
|
||||
}
|
||||
private string _soundGunshot;
|
||||
public string SoundEmpty => _soundEmpty;
|
||||
private string _soundEmpty;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _fireRateSelector, "currentSelector", FireRateSelector.Safety);
|
||||
serializer.DataField(ref _fireRate, "fireRate", 2.0f);
|
||||
|
||||
// This hard-to-read area's dealing with recoil
|
||||
// Use degrees in yaml as it's easier to read compared to "0.0125f"
|
||||
if (serializer.Reading)
|
||||
{
|
||||
var minAngle = serializer.ReadDataField("minAngle", 0) / 2;
|
||||
_minAngle = Angle.FromDegrees(minAngle);
|
||||
// Random doubles it as it's +/- so uhh we'll just half it here for readability
|
||||
var maxAngle = serializer.ReadDataField("maxAngle", 45) / 2;
|
||||
_maxAngle = Angle.FromDegrees(maxAngle);
|
||||
var angleIncrease = serializer.ReadDataField("angleIncrease", (40 / _fireRate));
|
||||
_angleIncrease = angleIncrease * (float) Math.PI / 180;
|
||||
var angleDecay = serializer.ReadDataField("angleDecay", (float) 20);
|
||||
_angleDecay = angleDecay * (float) Math.PI / 180;
|
||||
serializer.DataField(ref _spreadRatio, "ammoSpreadRatio", 1.0f);
|
||||
|
||||
// FireRate options
|
||||
var allFireRates = serializer.ReadDataField("allSelectors", new List<FireRateSelector>());
|
||||
foreach (var fireRate in allFireRates)
|
||||
{
|
||||
_allRateSelectors |= fireRate;
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
serializer.DataField(ref _canMuzzleFlash, "canMuzzleFlash", true);
|
||||
// Sounds
|
||||
serializer.DataField(ref _soundGunshot, "soundGunshot", null);
|
||||
serializer.DataField(ref _soundEmpty, "soundEmpty", "/Audio/Guns/Empty/empty.ogg");
|
||||
}
|
||||
|
||||
public override void OnAdd()
|
||||
{
|
||||
base.OnAdd();
|
||||
var rangedWeapon = Owner.GetComponent<ServerRangedWeaponComponent>();
|
||||
rangedWeapon.Barrel = this;
|
||||
rangedWeapon.FireHandler += Fire;
|
||||
rangedWeapon.WeaponCanFireHandler += WeaponCanFire;
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
var rangedWeapon = Owner.GetComponent<ServerRangedWeaponComponent>();
|
||||
rangedWeapon.Barrel = null;
|
||||
rangedWeapon.FireHandler -= Fire;
|
||||
rangedWeapon.WeaponCanFireHandler -= WeaponCanFire;
|
||||
}
|
||||
|
||||
private Angle GetRecoilAngle(Angle direction)
|
||||
{
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
var timeSinceLastFire = (currentTime - _lastFire).TotalSeconds;
|
||||
var newTheta = Math.Clamp(_currentAngle.Theta + _angleIncrease - _angleDecay * timeSinceLastFire, _minAngle.Theta, _maxAngle.Theta);
|
||||
_currentAngle = new Angle(newTheta);
|
||||
|
||||
var random = (_robustRandom.NextDouble() - 0.5) * 2;
|
||||
var angle = Angle.FromDegrees(direction.Degrees + _currentAngle.Degrees * random);
|
||||
return angle;
|
||||
}
|
||||
|
||||
public abstract bool UseEntity(UseEntityEventArgs eventArgs);
|
||||
public abstract bool InteractUsing(InteractUsingEventArgs eventArgs);
|
||||
|
||||
public void ChangeFireSelector(FireRateSelector rateSelector)
|
||||
{
|
||||
if ((rateSelector & AllRateSelectors) != 0)
|
||||
{
|
||||
_fireRateSelector = rateSelector;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
protected virtual bool WeaponCanFire()
|
||||
{
|
||||
// If the ServerRangedWeaponComponent gets re-done probably need to add the checks here
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Fire(IEntity shooter, GridCoordinates target)
|
||||
{
|
||||
var soundSystem = EntitySystem.Get<AudioSystem>();
|
||||
if (ShotsLeft == 0)
|
||||
{
|
||||
if (_soundEmpty != null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var ammo = PeekAmmo();
|
||||
var projectile = TakeProjectile();
|
||||
if (projectile == null)
|
||||
{
|
||||
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point firing is confirmed
|
||||
var worldPosition = IoCManager.Resolve<IMapManager>().GetGrid(target.GridID).LocalToWorld(target).Position;
|
||||
var direction = (worldPosition - shooter.Transform.WorldPosition).ToAngle();
|
||||
var angle = GetRecoilAngle(direction);
|
||||
// This should really be client-side but for now we'll just leave it here
|
||||
if (shooter.TryGetComponent(out CameraRecoilComponent recoilComponent))
|
||||
{
|
||||
recoilComponent.Kick(-angle.ToVec() * 0.15f);
|
||||
}
|
||||
|
||||
// This section probably needs tweaking so there can be caseless hitscan etc.
|
||||
if (projectile.TryGetComponent(out HitscanComponent hitscan))
|
||||
{
|
||||
FireHitscan(shooter, hitscan, angle);
|
||||
}
|
||||
else if (projectile.HasComponent<ProjectileComponent>())
|
||||
{
|
||||
var ammoComponent = ammo.GetComponent<AmmoComponent>();
|
||||
|
||||
FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity);
|
||||
|
||||
if (CanMuzzleFlash)
|
||||
{
|
||||
ammoComponent.MuzzleFlash(Owner.Transform.GridPosition, angle);
|
||||
}
|
||||
|
||||
if (ammoComponent.Caseless)
|
||||
{
|
||||
ammo.Delete();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid types
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
soundSystem.PlayAtCoords(_soundGunshot, Owner.Transform.GridPosition);
|
||||
_lastFire = _gameTiming.CurTime;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops a single cartridge / shell
|
||||
/// Made as a static function just because multiple places need it
|
||||
/// </summary>
|
||||
/// <param name="entity"></param>
|
||||
/// <param name="playSound"></param>
|
||||
/// <param name="robustRandom"></param>
|
||||
/// <param name="prototypeManager"></param>
|
||||
/// <param name="ejectDirections"></param>
|
||||
public static void EjectCasing(
|
||||
IEntity entity,
|
||||
bool playSound = true,
|
||||
IRobustRandom robustRandom = null,
|
||||
IPrototypeManager prototypeManager = null,
|
||||
Direction[] ejectDirections = null)
|
||||
{
|
||||
if (robustRandom == null)
|
||||
{
|
||||
robustRandom = IoCManager.Resolve<IRobustRandom>();
|
||||
}
|
||||
|
||||
if (ejectDirections == null)
|
||||
{
|
||||
ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
|
||||
}
|
||||
|
||||
const float ejectOffset = 0.2f;
|
||||
var ammo = entity.GetComponent<AmmoComponent>();
|
||||
var offsetPos = (robustRandom.NextFloat() * ejectOffset, robustRandom.NextFloat() * ejectOffset);
|
||||
entity.Transform.GridPosition = entity.Transform.GridPosition.Offset(offsetPos);
|
||||
entity.Transform.LocalRotation = robustRandom.Pick(ejectDirections).ToAngle();
|
||||
|
||||
if (ammo.SoundCollectionEject == null || !playSound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (prototypeManager == null)
|
||||
{
|
||||
prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
}
|
||||
|
||||
var soundCollection = prototypeManager.Index<SoundCollectionPrototype>(ammo.SoundCollectionEject);
|
||||
var randomFile = robustRandom.Pick(soundCollection.PickFiles);
|
||||
EntitySystem.Get<AudioSystem>().PlayAtCoords(randomFile, entity.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops multiple cartridges / shells on the floor
|
||||
/// Wraps EjectCasing to make it less toxic for bulk ejections
|
||||
/// </summary>
|
||||
/// <param name="entities"></param>
|
||||
public static void EjectCasings(IEnumerable<IEntity> entities)
|
||||
{
|
||||
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
var ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
|
||||
var soundPlayCount = 0;
|
||||
var playSound = true;
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
EjectCasing(entity, playSound, robustRandom, prototypeManager, ejectDirections);
|
||||
soundPlayCount++;
|
||||
if (soundPlayCount > 3)
|
||||
{
|
||||
playSound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Firing
|
||||
/// <summary>
|
||||
/// Handles firing one or many projectiles
|
||||
/// </summary>
|
||||
private void FireProjectiles(IEntity shooter, IEntity baseProjectile, int count, float evenSpreadAngle, Angle angle, float velocity)
|
||||
{
|
||||
List<Angle> sprayAngleChange = null;
|
||||
if (count > 1)
|
||||
{
|
||||
evenSpreadAngle *= _spreadRatio;
|
||||
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
|
||||
}
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
IEntity projectile;
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
projectile = baseProjectile;
|
||||
}
|
||||
else
|
||||
{
|
||||
projectile =
|
||||
Owner.EntityManager.SpawnEntity(baseProjectile.Prototype.ID, Owner.Transform.GridPosition);
|
||||
}
|
||||
|
||||
Angle projectileAngle;
|
||||
|
||||
if (sprayAngleChange != null)
|
||||
{
|
||||
projectileAngle = angle + sprayAngleChange[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
projectileAngle = angle;
|
||||
}
|
||||
|
||||
var physicsComponent = projectile.GetComponent<PhysicsComponent>();
|
||||
physicsComponent.Status = BodyStatus.InAir;
|
||||
projectile.Transform.GridPosition = Owner.Transform.GridPosition;
|
||||
|
||||
var projectileComponent = projectile.GetComponent<ProjectileComponent>();
|
||||
projectileComponent.IgnoreEntity(shooter);
|
||||
projectile.GetComponent<PhysicsComponent>().LinearVelocity = projectileAngle.ToVec() * velocity;
|
||||
projectile.Transform.LocalRotation = projectileAngle.Theta;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(IEntity shooter, HitscanComponent hitscan, Angle angle)
|
||||
{
|
||||
var ray = new CollisionRay(Owner.Transform.GridPosition.Position, angle.ToVec(), (int) hitscan.CollisionMask);
|
||||
var physicsManager = IoCManager.Resolve<IPhysicsManager>();
|
||||
var rayCastResults = physicsManager.IntersectRay(Owner.Transform.MapID, ray, hitscan.MaxLength, shooter, false).ToList();
|
||||
|
||||
if (rayCastResults.Count >= 1)
|
||||
{
|
||||
var result = rayCastResults[0];
|
||||
var distance = result.HitEntity != null ? result.Distance : hitscan.MaxLength;
|
||||
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
|
||||
|
||||
if (result.HitEntity == null || !result.HitEntity.TryGetComponent(out DamageableComponent damageable))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
damageable.TakeDamage(
|
||||
hitscan.DamageType,
|
||||
(int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero),
|
||||
Owner,
|
||||
shooter);
|
||||
//I used Math.Round over Convert.toInt32, as toInt32 always rounds to
|
||||
//even numbers if halfway between two numbers, rather than rounding to nearest
|
||||
}
|
||||
else
|
||||
{
|
||||
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class HitscanWeaponCapacitorComponent : PowerCellComponent
|
||||
{
|
||||
private AppearanceComponent _appearance;
|
||||
|
||||
public override string Name => "HitscanWeaponCapacitor";
|
||||
|
||||
public override float Charge
|
||||
{
|
||||
get => base.Charge;
|
||||
set
|
||||
{
|
||||
base.Charge = value;
|
||||
_updateAppearance();
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Charge = Capacity;
|
||||
Owner.TryGetComponent(out _appearance);
|
||||
|
||||
}
|
||||
|
||||
public float GetChargeFrom(float toDeduct)
|
||||
{
|
||||
//Use this function when you want to shoot even though you don't have enough energy for basecost
|
||||
ChargeChanged();
|
||||
var chargeChangedBy = Math.Min(this.Charge, toDeduct);
|
||||
this.DeductCharge(chargeChangedBy);
|
||||
_updateAppearance();
|
||||
return chargeChangedBy;
|
||||
}
|
||||
|
||||
public void FillFrom(PowerStorageComponent battery)
|
||||
{
|
||||
var capacitorPowerDeficit = this.Capacity - this.Charge;
|
||||
if (battery.CanDeductCharge(capacitorPowerDeficit))
|
||||
{
|
||||
battery.DeductCharge(capacitorPowerDeficit);
|
||||
this.AddCharge(capacitorPowerDeficit);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddCharge(battery.Charge);
|
||||
battery.DeductCharge(battery.Charge);
|
||||
}
|
||||
_updateAppearance();
|
||||
}
|
||||
|
||||
private void _updateAppearance()
|
||||
{
|
||||
_appearance?.SetData(PowerCellVisuals.ChargeLevel, Charge / Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.EntitySystemMessages;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Physics;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class HitscanWeaponComponent : Component, IInteractUsing
|
||||
{
|
||||
private const float MaxLength = 20;
|
||||
public override string Name => "HitscanWeapon";
|
||||
|
||||
string _spritename;
|
||||
private int _damage;
|
||||
private int _baseFireCost;
|
||||
private float _lowerChargeLimit;
|
||||
private string _fireSound;
|
||||
|
||||
//As this is a component that sits on the weapon rather than a static value
|
||||
//we just declare the field and then use GetComponent later to actually get it.
|
||||
//Do remember to add it in both the .yaml prototype and the factory in EntryPoint.cs
|
||||
//Otherwise you will get errors
|
||||
private HitscanWeaponCapacitorComponent capacitorComponent;
|
||||
|
||||
public int Damage => _damage;
|
||||
|
||||
public int BaseFireCost => _baseFireCost;
|
||||
|
||||
public HitscanWeaponCapacitorComponent CapacitorComponent => capacitorComponent;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _spritename, "fireSprite", "Objects/laser.png");
|
||||
serializer.DataField(ref _damage, "damage", 10);
|
||||
serializer.DataField(ref _baseFireCost, "baseFireCost", 300);
|
||||
serializer.DataField(ref _lowerChargeLimit, "lowerChargeLimit", 10);
|
||||
serializer.DataField(ref _fireSound, "fireSound", "/Audio/laser.ogg");
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var rangedWeapon = Owner.GetComponent<RangedWeaponComponent>();
|
||||
capacitorComponent = Owner.GetComponent<HitscanWeaponCapacitorComponent>();
|
||||
rangedWeapon.FireHandler = Fire;
|
||||
|
||||
}
|
||||
|
||||
public bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.TryGetComponent(out PowerStorageComponent component))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (capacitorComponent.Full)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Capacitor at max charge");
|
||||
return false;
|
||||
}
|
||||
capacitorComponent.FillFrom(component);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Fire(IEntity user, GridCoordinates clickLocation)
|
||||
{
|
||||
if (capacitorComponent.Charge < _lowerChargeLimit)
|
||||
{//If capacitor has less energy than the lower limit, do nothing
|
||||
return;
|
||||
}
|
||||
float energyModifier = capacitorComponent.GetChargeFrom(_baseFireCost) / _baseFireCost;
|
||||
var userPosition = user.Transform.WorldPosition; //Remember world positions are ephemeral and can only be used instantaneously
|
||||
var angle = new Angle(clickLocation.Position - userPosition);
|
||||
|
||||
var ray = new CollisionRay(userPosition, angle.ToVec(), (int)(CollisionGroup.Opaque));
|
||||
var rayCastResults = IoCManager.Resolve<IPhysicsManager>().IntersectRay(user.Transform.MapID, ray, MaxLength, user, returnOnFirstHit: false).ToList();
|
||||
|
||||
//The first result is guaranteed to be the closest one
|
||||
if (rayCastResults.Count >= 1)
|
||||
{
|
||||
Hit(rayCastResults[0], energyModifier, user);
|
||||
AfterEffects(user, rayCastResults[0].Distance, angle, energyModifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
AfterEffects(user, MaxLength, angle, energyModifier);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Hit(RayCastResults ray, float damageModifier, IEntity user = null)
|
||||
{
|
||||
if (ray.HitEntity != null && ray.HitEntity.TryGetComponent(out DamageableComponent damage))
|
||||
{
|
||||
damage.TakeDamage(DamageType.Heat, (int)Math.Round(_damage * damageModifier, MidpointRounding.AwayFromZero), Owner, user);
|
||||
//I used Math.Round over Convert.toInt32, as toInt32 always rounds to
|
||||
//even numbers if halfway between two numbers, rather than rounding to nearest
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void AfterEffects(IEntity user, float distance, Angle angle, float energyModifier)
|
||||
{
|
||||
var time = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
var offset = angle.ToVec() * distance / 2;
|
||||
var message = new EffectSystemMessage
|
||||
{
|
||||
EffectSprite = _spritename,
|
||||
Born = time,
|
||||
DeathTime = time + TimeSpan.FromSeconds(1),
|
||||
Size = new Vector2(distance, 1f),
|
||||
Coordinates = user.Transform.GridPosition.Translated(offset),
|
||||
//Rotated from east facing
|
||||
Rotation = (float) angle.Theta,
|
||||
ColorDelta = new Vector4(0, 0, 0, -1500f),
|
||||
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), energyModifier),
|
||||
|
||||
Shaded = false
|
||||
};
|
||||
EntitySystem.Get<EffectSystem>().CreateParticle(message);
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_fireSound, Owner, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class AmmoBoxComponent : Component, IInteractUsing, IMapInit
|
||||
// TODO: Potential improvements:
|
||||
// Add verbs for stack splitting
|
||||
// Behaviour is largely the same as BallisticMagazine except you can't insert it into a gun.
|
||||
{
|
||||
public override string Name => "AmmoBox";
|
||||
private BallisticCaliber _caliber;
|
||||
private int _capacity;
|
||||
[ViewVariables] private int _availableSpawnCount;
|
||||
|
||||
[ViewVariables] private readonly Stack<IEntity> _loadedBullets = new Stack<IEntity>();
|
||||
|
||||
[ViewVariables]
|
||||
public string FillType => _fillType;
|
||||
private string _fillType;
|
||||
|
||||
[ViewVariables] private Container _bulletContainer;
|
||||
[ViewVariables] private AppearanceComponent _appearance;
|
||||
|
||||
[ViewVariables] public int Capacity => _capacity;
|
||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
||||
[ViewVariables] public int CountLeft => _loadedBullets.Count + _availableSpawnCount;
|
||||
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _fillType, "fill", null);
|
||||
serializer.DataField(ref _capacity, "capacity", 30);
|
||||
serializer.DataField(ref _availableSpawnCount, "availableSpawnCount", Capacity);
|
||||
}
|
||||
|
||||
private void _updateAppearance()
|
||||
{
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoLeft, CountLeft);
|
||||
}
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
_availableSpawnCount = Capacity;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_appearance = Owner.GetComponent<AppearanceComponent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
_bulletContainer =
|
||||
ContainerManagerComponent.Ensure<Container>("box_bullet_container", Owner, out var existed);
|
||||
|
||||
if (existed)
|
||||
{
|
||||
foreach (var entity in _bulletContainer.ContainedEntities)
|
||||
{
|
||||
_loadedBullets.Push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
_updateAppearance();
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoCapacity, Capacity);
|
||||
}
|
||||
|
||||
AmmoBoxTransferPopupMessage CanTransferFrom(IEntity source)
|
||||
{
|
||||
// Currently the below duplicates mags but at some stage these will likely differ
|
||||
if (source.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
if (magazineComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLeft == Capacity)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (magazineComponent.CountLoaded == 0)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoBoxTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
if (source.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
if (boxComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLeft == Capacity)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (boxComponent.CountLeft == 0)
|
||||
{
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoBoxTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
return new AmmoBoxTransferPopupMessage(result: false, message: "");
|
||||
}
|
||||
|
||||
// TODO: Potentially abstract out to reduce duplicate structs
|
||||
private struct AmmoBoxTransferPopupMessage
|
||||
{
|
||||
public readonly bool Result;
|
||||
public readonly string Message;
|
||||
|
||||
public AmmoBoxTransferPopupMessage(bool result, string message)
|
||||
{
|
||||
Result = result;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
var ammoBoxTransfer = CanTransferFrom(eventArgs.Using);
|
||||
if (ammoBoxTransfer.Result) {
|
||||
IEntity bullet;
|
||||
if (eventArgs.Using.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
int fillCount = Math.Min(magazineComponent.CountLoaded, Capacity - CountLeft);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = magazineComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
if (eventArgs.Using.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
int fillCount = Math.Min(boxComponent.CountLeft, Capacity - CountLeft);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = boxComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User, ammoBoxTransfer.Message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void AddBullet(IEntity bullet)
|
||||
{
|
||||
if (Owner.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
magazineComponent.AddBullet(bullet);
|
||||
return;
|
||||
}
|
||||
if (!bullet.TryGetComponent(out BallisticBulletComponent component))
|
||||
{
|
||||
throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (component.Caliber != Caliber)
|
||||
{
|
||||
throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (CountLeft >= Capacity)
|
||||
{
|
||||
throw new InvalidOperationException("Box is full.");
|
||||
}
|
||||
|
||||
_bulletContainer.Insert(bullet);
|
||||
_loadedBullets.Push(bullet);
|
||||
_updateAppearance();
|
||||
}
|
||||
|
||||
public IEntity TakeBullet()
|
||||
{
|
||||
IEntity bullet;
|
||||
if (Owner.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
bullet = magazineComponent.TakeBullet();
|
||||
return bullet;
|
||||
}
|
||||
if (_loadedBullets.Count == 0)
|
||||
{
|
||||
if (_availableSpawnCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_availableSpawnCount -= 1;
|
||||
bullet = Owner.EntityManager.SpawnEntity(FillType, Owner.Transform.GridPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
bullet = _loadedBullets.Pop();
|
||||
_bulletContainer.Remove(bullet);
|
||||
}
|
||||
|
||||
_updateAppearance();
|
||||
return bullet;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Passes information about the projectiles to be fired by AmmoWeapons
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class BallisticBulletComponent : Component
|
||||
{
|
||||
public override string Name => "BallisticBullet";
|
||||
|
||||
private BallisticCaliber _caliber;
|
||||
/// <summary>
|
||||
/// Cartridge calibre, restricts what AmmoWeapons this ammo can be fired from.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public BallisticCaliber Caliber { get => _caliber; set => _caliber = value; }
|
||||
|
||||
private string _projectileID;
|
||||
/// <summary>
|
||||
/// YAML ID of the projectiles to be created when firing this ammo.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string ProjectileID { get => _projectileID; set => _projectileID = value; }
|
||||
|
||||
private int _projectilesFired;
|
||||
/// <summary>
|
||||
/// How many copies of the projectile are shot.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int ProjectilesFired { get => _projectilesFired; set => _projectilesFired = value; }
|
||||
|
||||
private float _spreadStdDev_Ammo;
|
||||
/// <summary>
|
||||
/// Weapons that fire projectiles from ammo types.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SpreadStdDev_Ammo { get => _spreadStdDev_Ammo; set => _spreadStdDev_Ammo = value; }
|
||||
|
||||
private float _evenSpreadAngle_Ammo;
|
||||
/// <summary>
|
||||
/// Arc angle of shotgun pellet spreads, only used if multiple projectiles are being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float EvenSpreadAngle_Ammo { get => _evenSpreadAngle_Ammo; set => _evenSpreadAngle_Ammo = value; }
|
||||
|
||||
private float _velocity_Ammo;
|
||||
/// <summary>
|
||||
/// Adds additional velocity to the projectile, on top of what it already has.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Velocity_Ammo { get => _velocity_Ammo; set => _velocity_Ammo = value; }
|
||||
|
||||
private bool _spent;
|
||||
/// <summary>
|
||||
/// If the ammo cartridge has been shot already.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Spent { get => _spent; set => _spent = value; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _projectileID, "projectile", null);
|
||||
serializer.DataField(ref _spent, "spent", false);
|
||||
serializer.DataField(ref _projectilesFired, "projectilesfired", 1);
|
||||
serializer.DataField(ref _spreadStdDev_Ammo, "ammostddev", 0);
|
||||
serializer.DataField(ref _evenSpreadAngle_Ammo, "ammospread", 0);
|
||||
serializer.DataField(ref _velocity_Ammo, "ammovelocity", 0);
|
||||
}
|
||||
}
|
||||
public enum BallisticCaliber
|
||||
{
|
||||
Unspecified = 0,
|
||||
// .32
|
||||
A32,
|
||||
// .357
|
||||
A357,
|
||||
// .44
|
||||
A44,
|
||||
// .45mm
|
||||
A45mm,
|
||||
// .50 cal
|
||||
A50,
|
||||
// 5.56mm
|
||||
A556mm,
|
||||
// 6.5mm
|
||||
A65mm,
|
||||
// 7.62mm
|
||||
A762mm,
|
||||
// 9mm
|
||||
A9mm,
|
||||
// 10mm
|
||||
A10mm,
|
||||
// 20mm
|
||||
A20mm,
|
||||
// 24mm
|
||||
A24mm,
|
||||
// 12g
|
||||
A12g,
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class BallisticMagazineComponent : Component, IMapInit, IInteractUsing
|
||||
{
|
||||
public override string Name => "BallisticMagazine";
|
||||
|
||||
// Stack of loaded bullets.
|
||||
[ViewVariables] private readonly Stack<IEntity> _loadedBullets = new Stack<IEntity>();
|
||||
private string _fillType;
|
||||
|
||||
[ViewVariables] private Container _bulletContainer;
|
||||
[ViewVariables] private AppearanceComponent _appearance;
|
||||
|
||||
private BallisticMagazineType _magazineType;
|
||||
private BallisticCaliber _caliber;
|
||||
private int _capacity;
|
||||
|
||||
[ViewVariables] public string FillType => _fillType;
|
||||
[ViewVariables] public BallisticMagazineType MagazineType => _magazineType;
|
||||
[ViewVariables] public BallisticCaliber Caliber => _caliber;
|
||||
[ViewVariables] public int Capacity => _capacity;
|
||||
|
||||
[ViewVariables] public int CountLoaded => _loadedBullets.Count + _availableSpawnCount;
|
||||
|
||||
[ViewVariables] private int _availableSpawnCount;
|
||||
|
||||
public event Action OnAmmoCountChanged;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _magazineType, "magazine", BallisticMagazineType.Unspecified);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _fillType, "fill", null);
|
||||
serializer.DataField(ref _capacity, "capacity", 20);
|
||||
serializer.DataField(ref _availableSpawnCount, "availableSpawnCount", Capacity);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_appearance = Owner.GetComponent<AppearanceComponent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
_bulletContainer =
|
||||
ContainerManagerComponent.Ensure<Container>("magazine_bullet_container", Owner, out var existed);
|
||||
|
||||
if (existed)
|
||||
{
|
||||
foreach (var entity in _bulletContainer.ContainedEntities)
|
||||
{
|
||||
_loadedBullets.Push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
|
||||
OnAmmoCountChanged?.Invoke();
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoCapacity, Capacity);
|
||||
}
|
||||
|
||||
public void AddBullet(IEntity bullet)
|
||||
{
|
||||
if (!bullet.TryGetComponent(out BallisticBulletComponent component))
|
||||
{
|
||||
throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (component.Caliber != Caliber)
|
||||
{
|
||||
throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
|
||||
}
|
||||
|
||||
if (CountLoaded >= Capacity)
|
||||
{
|
||||
throw new InvalidOperationException("Magazine is full.");
|
||||
}
|
||||
|
||||
_bulletContainer.Insert(bullet);
|
||||
_loadedBullets.Push(bullet);
|
||||
UpdateAppearance();
|
||||
OnAmmoCountChanged?.Invoke();
|
||||
}
|
||||
|
||||
public IEntity TakeBullet()
|
||||
{
|
||||
IEntity bullet;
|
||||
if (_loadedBullets.Count == 0)
|
||||
{
|
||||
if (_availableSpawnCount == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_availableSpawnCount -= 1;
|
||||
bullet = Owner.EntityManager.SpawnEntity(FillType, Owner.Transform.GridPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
bullet = _loadedBullets.Pop();
|
||||
_bulletContainer.Remove(bullet);
|
||||
}
|
||||
|
||||
UpdateAppearance();
|
||||
OnAmmoCountChanged?.Invoke();
|
||||
return bullet;
|
||||
}
|
||||
|
||||
// TODO: Allow putting individual casings into mag (also box)
|
||||
AmmoMagTransferPopupMessage CanTransferFrom(IEntity source)
|
||||
{
|
||||
// Currently the below duplicates box but at some stage these will likely differ
|
||||
if (source.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
if (magazineComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLoaded == Capacity)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (magazineComponent.CountLoaded == 0)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoMagTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
// If box
|
||||
if (source.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
if (boxComponent.Caliber != Caliber)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Wrong caliber");
|
||||
}
|
||||
|
||||
if (CountLoaded == Capacity)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "Already full");
|
||||
}
|
||||
|
||||
if (boxComponent.CountLeft == 0)
|
||||
{
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "No ammo to transfer");
|
||||
}
|
||||
|
||||
return new AmmoMagTransferPopupMessage(result: true, message: "");
|
||||
}
|
||||
|
||||
return new AmmoMagTransferPopupMessage(result: false, message: "");
|
||||
}
|
||||
|
||||
// TODO: Potentially abstract out to reduce duplicate structs
|
||||
private struct AmmoMagTransferPopupMessage
|
||||
{
|
||||
public readonly bool Result;
|
||||
public readonly string Message;
|
||||
|
||||
public AmmoMagTransferPopupMessage(bool result, string message)
|
||||
{
|
||||
Result = result;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
var ammoMagTransfer = CanTransferFrom(eventArgs.Using);
|
||||
if (ammoMagTransfer.Result) {
|
||||
IEntity bullet;
|
||||
if (eventArgs.Using.TryGetComponent(out BallisticMagazineComponent magazineComponent))
|
||||
{
|
||||
int fillCount = Math.Min(magazineComponent.CountLoaded, Capacity - CountLoaded);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = magazineComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
if (eventArgs.Using.TryGetComponent(out AmmoBoxComponent boxComponent))
|
||||
{
|
||||
int fillCount = Math.Min(boxComponent.CountLeft, Capacity - CountLoaded);
|
||||
for (int i = 0; i < fillCount; i++)
|
||||
{
|
||||
bullet = boxComponent.TakeBullet();
|
||||
AddBullet(bullet);
|
||||
}
|
||||
eventArgs.User.PopupMessage(eventArgs.User, $"Transferred {fillCount} rounds");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User, ammoMagTransfer.Message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
_appearance.SetData(BallisticMagazineVisuals.AmmoLeft, CountLoaded);
|
||||
}
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
_availableSpawnCount = Capacity;
|
||||
}
|
||||
}
|
||||
|
||||
public enum BallisticMagazineType
|
||||
{
|
||||
Unspecified = 0,
|
||||
|
||||
// .32
|
||||
A32,
|
||||
|
||||
// .357
|
||||
A357,
|
||||
|
||||
// .44
|
||||
A44,
|
||||
|
||||
// .45mm
|
||||
A45mm,
|
||||
|
||||
// .50 cal
|
||||
A50,
|
||||
|
||||
// 5.56mm
|
||||
A556mm,
|
||||
|
||||
// 6.5mm
|
||||
A65mm,
|
||||
|
||||
// 7.62mm
|
||||
A762mm,
|
||||
Maxim,
|
||||
|
||||
// 9mm
|
||||
A9mm,
|
||||
A9mmSMG,
|
||||
A9mmTopMounted,
|
||||
|
||||
// 10mm
|
||||
A10mm,
|
||||
A10mmSMG,
|
||||
|
||||
// 20mm
|
||||
A20mm,
|
||||
|
||||
// 24mm
|
||||
A24mm,
|
||||
|
||||
// 12g
|
||||
A12g,
|
||||
}
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Content.Shared.Interfaces;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Guns that have a magazine.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class BallisticMagazineWeaponComponent : BallisticWeaponComponent, IUse, IInteractUsing, IMapInit
|
||||
{
|
||||
private const float BulletOffset = 0.2f;
|
||||
|
||||
public override string Name => "BallisticMagazineWeapon";
|
||||
public override uint? NetID => ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON;
|
||||
|
||||
[ViewVariables] private string _defaultMagazine;
|
||||
|
||||
public ContainerSlot MagazineSlot => _magazineSlot;
|
||||
[ViewVariables] private ContainerSlot _magazineSlot;
|
||||
private List<BallisticMagazineType> _magazineTypes;
|
||||
|
||||
[ViewVariables] public List<BallisticMagazineType> MagazineTypes => _magazineTypes;
|
||||
[ViewVariables] private IEntity Magazine => _magazineSlot.ContainedEntity;
|
||||
|
||||
#pragma warning disable 649
|
||||
[Dependency] private readonly IRobustRandom _bulletDropRandom;
|
||||
#pragma warning restore 649
|
||||
[ViewVariables] private string _magInSound;
|
||||
[ViewVariables] private string _magOutSound;
|
||||
[ViewVariables] private string _autoEjectSound;
|
||||
[ViewVariables] private bool _autoEjectMagazine;
|
||||
[ViewVariables] private AppearanceComponent _appearance;
|
||||
|
||||
private static readonly Direction[] RandomBulletDirs =
|
||||
{
|
||||
Direction.North,
|
||||
Direction.East,
|
||||
Direction.South,
|
||||
Direction.West
|
||||
};
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _magazineTypes, "magazines",
|
||||
new List<BallisticMagazineType> {BallisticMagazineType.Unspecified});
|
||||
serializer.DataField(ref _defaultMagazine, "default_magazine", null);
|
||||
serializer.DataField(ref _autoEjectMagazine, "auto_eject_magazine", false);
|
||||
serializer.DataField(ref _autoEjectSound, "sound_auto_eject", null);
|
||||
serializer.DataField(ref _magInSound, "sound_magazine_in", null);
|
||||
serializer.DataField(ref _magOutSound, "sound_magazine_out", null);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_appearance = Owner.GetComponent<AppearanceComponent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
_magazineSlot = ContainerManagerComponent.Ensure<ContainerSlot>("ballistic_gun_magazine", Owner);
|
||||
if (Magazine != null)
|
||||
{
|
||||
// Already got magazine from loading a container.
|
||||
Magazine.GetComponent<BallisticMagazineComponent>().OnAmmoCountChanged += MagazineAmmoCountChanged;
|
||||
}
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public bool InsertMagazine(IEntity magazine, bool playSound = true)
|
||||
{
|
||||
if (!magazine.TryGetComponent(out BallisticMagazineComponent magazinetype))
|
||||
{
|
||||
throw new ArgumentException("Not a magazine", nameof(magazine));
|
||||
}
|
||||
if (!MagazineTypes.Contains(magazinetype.MagazineType))
|
||||
{
|
||||
throw new ArgumentException("Wrong magazine type", nameof(magazine));
|
||||
}
|
||||
if (!_magazineSlot.Insert(magazine))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_magInSound != null && playSound)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_magInSound, Owner);
|
||||
}
|
||||
magazinetype.OnAmmoCountChanged += MagazineAmmoCountChanged;
|
||||
if (GetChambered(0) == null)
|
||||
{
|
||||
// No bullet in chamber, load one from magazine.
|
||||
var bullet = magazinetype.TakeBullet();
|
||||
if (bullet != null)
|
||||
{
|
||||
LoadIntoChamber(0, bullet);
|
||||
}
|
||||
}
|
||||
UpdateAppearance();
|
||||
Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool EjectMagazine(bool playSound = true)
|
||||
{
|
||||
var entity = Magazine;
|
||||
if (entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (_magazineSlot.Remove(entity))
|
||||
{
|
||||
entity.Transform.GridPosition = Owner.Transform.GridPosition;
|
||||
if (_magOutSound != null && playSound)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_magOutSound, Owner, AudioParams.Default.WithVolume(20));
|
||||
}
|
||||
UpdateAppearance();
|
||||
Dirty();
|
||||
entity.GetComponent<BallisticMagazineComponent>().OnAmmoCountChanged -= MagazineAmmoCountChanged;
|
||||
return true;
|
||||
}
|
||||
UpdateAppearance();
|
||||
Dirty();
|
||||
return false;
|
||||
}
|
||||
|
||||
// these are complete strings for the sake of the shared string dict
|
||||
[UsedImplicitly]
|
||||
private static readonly string[] _bulletDropSounds =
|
||||
{
|
||||
"/Audio/Guns/Casings/casingfall1.ogg",
|
||||
"/Audio/Guns/Casings/casingfall2.ogg",
|
||||
"/Audio/Guns/Casings/casingfall3.ogg"
|
||||
};
|
||||
|
||||
protected override void CycleChamberedBullet(int chamber)
|
||||
{
|
||||
DebugTools.Assert(chamber == 0);
|
||||
|
||||
// Eject chambered bullet.
|
||||
var entity = RemoveFromChamber(chamber);
|
||||
if (entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var offsetPos = (CalcBulletOffset(), CalcBulletOffset());
|
||||
entity.Transform.GridPosition = Owner.Transform.GridPosition.Offset(offsetPos);
|
||||
entity.Transform.LocalRotation = _bulletDropRandom.Pick(RandomBulletDirs).ToAngle();
|
||||
var bulletDropNext = _bulletDropRandom.Next(1, 3);
|
||||
var effect = _bulletDropSounds[bulletDropNext];
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(effect, Owner, AudioParams.Default.WithVolume(-3));
|
||||
|
||||
if (Magazine != null)
|
||||
{
|
||||
var magComponent = Magazine.GetComponent<BallisticMagazineComponent>();
|
||||
var bullet = magComponent.TakeBullet();
|
||||
if (bullet != null)
|
||||
{
|
||||
LoadIntoChamber(0, bullet);
|
||||
}
|
||||
|
||||
if (magComponent.CountLoaded == 0 && _autoEjectMagazine)
|
||||
{
|
||||
DoAutoEject();
|
||||
}
|
||||
}
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private float CalcBulletOffset()
|
||||
{
|
||||
return _bulletDropRandom.NextFloat() * (BulletOffset * 2) - BulletOffset;
|
||||
}
|
||||
|
||||
private void DoAutoEject()
|
||||
{
|
||||
SendNetworkMessage(new BmwComponentAutoEjectedMessage());
|
||||
EjectMagazine();
|
||||
if (_autoEjectSound != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_autoEjectSound, Owner, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public bool UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
var ret = EjectMagazine();
|
||||
if (ret)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Magazine ejected");
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "No magazine");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.Using.TryGetComponent(out BallisticMagazineComponent component))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Magazine != null)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Already got a magazine.");
|
||||
return false;
|
||||
}
|
||||
if (!MagazineTypes.Contains(component.MagazineType))
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, "Magazine doesn't fit.");
|
||||
return false;
|
||||
}
|
||||
return InsertMagazine(eventArgs.Using);
|
||||
}
|
||||
|
||||
private void MagazineAmmoCountChanged()
|
||||
{
|
||||
Dirty();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (Magazine != null)
|
||||
{
|
||||
var comp = Magazine.GetComponent<BallisticMagazineComponent>();
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, comp.CountLoaded);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoCapacity, comp.Capacity);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.MagazineLoaded, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, 0);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.AmmoLeft, 0);
|
||||
_appearance.SetData(BallisticMagazineWeaponVisuals.MagazineLoaded, false);
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
var chambered = GetChambered(0) != null;
|
||||
(int, int)? count = null;
|
||||
if (Magazine != null)
|
||||
{
|
||||
var magComponent = Magazine.GetComponent<BallisticMagazineComponent>();
|
||||
count = (magComponent.CountLoaded, magComponent.Capacity);
|
||||
}
|
||||
return new BallisticMagazineWeaponComponentState(chambered, count);
|
||||
}
|
||||
|
||||
[Verb]
|
||||
public sealed class EjectMagazineVerb : Verb<BallisticMagazineWeaponComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BallisticMagazineWeaponComponent component, VerbData data)
|
||||
{
|
||||
if (component.Magazine == null)
|
||||
{
|
||||
data.Text = "Eject magazine (magazine missing)";
|
||||
data.Visibility = VerbVisibility.Disabled;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = "Eject magazine";
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BallisticMagazineWeaponComponent component)
|
||||
{
|
||||
component.EjectMagazine();
|
||||
}
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_defaultMagazine != null)
|
||||
{
|
||||
var magazine = Owner.EntityManager.SpawnEntity(_defaultMagazine, Owner.Transform.GridPosition);
|
||||
InsertMagazine(magazine, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
using Content.Server.GameObjects.Components.Sound;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles firing projectiles from a contained <see cref="BallisticBulletComponent" />.
|
||||
/// </summary>
|
||||
public abstract class BallisticWeaponComponent : BaseProjectileWeaponComponent
|
||||
{
|
||||
private Chamber[] _chambers;
|
||||
|
||||
/// <summary>
|
||||
/// Number of chambers created during initialization.
|
||||
/// </summary>
|
||||
private int _chamberCount;
|
||||
|
||||
[ViewVariables]
|
||||
private BallisticCaliber _caliber ;
|
||||
/// <summary>
|
||||
/// What type of ammo this gun can fire.
|
||||
/// </summary>
|
||||
|
||||
private string _soundGunEmpty;
|
||||
/// <summary>
|
||||
/// Sound played when trying to shoot if there is no ammo available.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SoundGunEmpty { get => _soundGunEmpty; set => _soundGunEmpty = value; }
|
||||
|
||||
private float _spreadStdDevGun;
|
||||
/// <summary>
|
||||
/// Increases the standard deviation of the ammo being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SpreadStdDevGun { get => _spreadStdDevGun; set => _spreadStdDevGun = value; }
|
||||
|
||||
private float _evenSpreadAngleGun;
|
||||
/// <summary>
|
||||
/// Increases the evenspread of the ammo being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float EvenSpreadAngleGun { get => _evenSpreadAngleGun; set => _evenSpreadAngleGun = value; }
|
||||
|
||||
private float _velocityGun;
|
||||
/// <summary>
|
||||
/// Increases the velocity of the ammo being fired.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float VelocityGun { get => _velocityGun; set => _velocityGun = value; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _soundGunEmpty, "sound_empty", "/Audio/Guns/Empty/empty.ogg");
|
||||
serializer.DataField(ref _spreadStdDevGun, "spreadstddev", 0);
|
||||
serializer.DataField(ref _evenSpreadAngleGun, "evenspread", 0);
|
||||
serializer.DataField(ref _velocityGun, "gunvelocity", 0);
|
||||
serializer.DataField(ref _caliber, "caliber", BallisticCaliber.Unspecified);
|
||||
serializer.DataField(ref _chamberCount, "chambers", 1);
|
||||
}
|
||||
|
||||
// for shared string dict, since we don't define these anywhere in content
|
||||
[UsedImplicitly]
|
||||
private static readonly string[] _ballisticsChambersStrings =
|
||||
{
|
||||
"ballistics_chamber_0",
|
||||
"ballistics_chamber_1",
|
||||
"ballistics_chamber_2",
|
||||
"ballistics_chamber_3",
|
||||
"ballistics_chamber_4",
|
||||
"ballistics_chamber_5",
|
||||
"ballistics_chamber_6",
|
||||
"ballistics_chamber_7",
|
||||
"ballistics_chamber_8",
|
||||
"ballistics_chamber_9",
|
||||
};
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
Owner.GetComponent<RangedWeaponComponent>().FireHandler = TryShoot;
|
||||
_chambers = new Chamber[_chamberCount];
|
||||
for (var i = 0; i < _chambers.Length; i++)
|
||||
{
|
||||
var container = ContainerManagerComponent.Ensure<ContainerSlot>($"ballistics_chamber_{i}", Owner);
|
||||
_chambers[i] = new Chamber(container);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires projectiles based on loaded ammo from entity to a coordinate.
|
||||
/// </summary>
|
||||
protected void TryShoot(IEntity user, GridCoordinates clickLocation)
|
||||
{
|
||||
var ammo = GetChambered(FirstChamber)?.GetComponent<BallisticBulletComponent>();
|
||||
CycleChamberedBullet(FirstChamber);
|
||||
if (ammo == null || ammo?.Spent == true || ammo?.Caliber != _caliber)
|
||||
{
|
||||
PlayEmptySound();
|
||||
return;
|
||||
}
|
||||
ammo.Spent = true;
|
||||
var total_stdev = _spreadStdDevGun + ammo.SpreadStdDev_Ammo;
|
||||
var final_evenspread = _evenSpreadAngleGun + ammo.EvenSpreadAngle_Ammo;
|
||||
var final_velocity = _velocityGun + ammo.Velocity_Ammo;
|
||||
FireAtCoord(user, clickLocation, ammo.ProjectileID, total_stdev, ammo.ProjectilesFired, final_evenspread, final_velocity);
|
||||
}
|
||||
|
||||
public IEntity GetChambered(int chamber) => _chambers[chamber].Slot.ContainedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the next ammo casing into the chamber.
|
||||
/// </summary>
|
||||
protected virtual void CycleChamberedBullet(int chamber) { }
|
||||
|
||||
public IEntity RemoveFromChamber(int chamber)
|
||||
{
|
||||
var c = _chambers[chamber];
|
||||
var loaded = c.Slot.ContainedEntity;
|
||||
if (loaded != null)
|
||||
{
|
||||
c.Slot.Remove(loaded);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
protected bool LoadIntoChamber(int chamber, IEntity bullet)
|
||||
{
|
||||
if (!bullet.TryGetComponent(out BallisticBulletComponent component))
|
||||
{
|
||||
throw new ArgumentException("entity isn't a bullet.", nameof(bullet));
|
||||
}
|
||||
if (component.Caliber != _caliber)
|
||||
{
|
||||
throw new ArgumentException("entity is of the wrong caliber.", nameof(bullet));
|
||||
}
|
||||
if (GetChambered(chamber) != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_chambers[chamber].Slot.Insert(bullet);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void PlayEmptySound() => EntitySystem.Get<AudioSystem>().PlayFromEntity(_soundGunEmpty, Owner);
|
||||
|
||||
protected sealed class Chamber
|
||||
{
|
||||
public Chamber(ContainerSlot slot)
|
||||
{
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
public ContainerSlot Slot { get; }
|
||||
}
|
||||
|
||||
private const int FirstChamber = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Projectiles;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
|
||||
{
|
||||
/// <summary>
|
||||
/// Methods to shoot projectiles.
|
||||
/// </summary>
|
||||
public abstract class BaseProjectileWeaponComponent : Component
|
||||
{
|
||||
private string _soundGunshot;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SoundGunshot
|
||||
{ get => _soundGunshot; set => _soundGunshot = value; }
|
||||
|
||||
#pragma warning disable 649
|
||||
[Dependency] private IRobustRandom _spreadRandom;
|
||||
#pragma warning restore 649
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _soundGunshot, "sound_gunshot", "/Audio/Guns/Gunshots/smg.ogg");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires projectile from an entity at a coordinate.
|
||||
/// </summary>
|
||||
protected void FireAtCoord(IEntity source, GridCoordinates coord, string projectileType, double spreadStdDev, int projectilesFired = 1, double evenSpreadAngle = 0, float velocity = 0)
|
||||
{
|
||||
var angle = GetAngleFromClickLocation(source, coord);
|
||||
FireAtAngle(source, angle, projectileType, spreadStdDev, projectilesFired, evenSpreadAngle, velocity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires projectile in the direction of an angle.
|
||||
/// </summary>
|
||||
protected void FireAtAngle(IEntity source, Angle angle, string projectileType = null, double spreadStdDev = 0, int projectilesFired = 1, double evenSpreadAngle = 0, float velocity = 0)
|
||||
{
|
||||
List<Angle> sprayanglechange = null;
|
||||
if (evenSpreadAngle != 0 & projectilesFired > 1)
|
||||
{
|
||||
sprayanglechange = Linspace(-evenSpreadAngle/2, evenSpreadAngle/2, projectilesFired);
|
||||
}
|
||||
for (var i = 1; i <= projectilesFired; i++)
|
||||
{
|
||||
Angle finalangle = angle + Angle.FromDegrees(_spreadRandom.NextGaussian(0, spreadStdDev)) + (sprayanglechange != null ? sprayanglechange[i - 1] : 0);
|
||||
var projectile = Owner.EntityManager.SpawnEntity(projectileType, Owner.Transform.GridPosition);
|
||||
projectile.Transform.GridPosition = source.Transform.GridPosition; //move projectile to entity it is being fired from
|
||||
projectile.GetComponent<ProjectileComponent>().IgnoreEntity(source);//make sure it doesn't hit the source entity
|
||||
var finalvelocity = projectile.GetComponent<ProjectileComponent>().Velocity + velocity;//add velocity
|
||||
var physicsComponent = projectile.GetComponent<PhysicsComponent>();
|
||||
physicsComponent.Status = BodyStatus.InAir;
|
||||
physicsComponent.LinearVelocity = finalangle.ToVec() * finalvelocity;//Rotate the bullets sprite to the correct direction
|
||||
projectile.Transform.LocalRotation = finalangle.Theta;
|
||||
}
|
||||
PlayFireSound();
|
||||
if (source.TryGetComponent(out CameraRecoilComponent recoil))
|
||||
{
|
||||
var recoilVec = angle.ToVec() * -0.15f;
|
||||
recoil.Kick(recoilVec);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayFireSound() => EntitySystem.Get<AudioSystem>().PlayFromEntity(_soundGunshot, Owner);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the angle from an entity to a coordinate.
|
||||
/// </summary>
|
||||
protected Angle GetAngleFromClickLocation(IEntity source, GridCoordinates clickLocation) => new Angle(clickLocation.Position - source.Transform.GridPosition.Position);
|
||||
|
||||
/// <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>
|
||||
protected List<Angle> Linspace(double start, double end, int intervals)
|
||||
{
|
||||
var linspace = new List<Angle> { };
|
||||
for (var i = 0; i <= intervals - 1; i++)
|
||||
{
|
||||
linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
|
||||
}
|
||||
return linspace;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,46 @@
|
||||
using System;
|
||||
using System;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class RangedWeaponComponent : SharedRangedWeaponComponent
|
||||
public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent, IHandSelected
|
||||
{
|
||||
private TimeSpan _lastFireTime;
|
||||
|
||||
|
||||
public Func<bool> WeaponCanFireHandler;
|
||||
public Func<IEntity, bool> UserCanFireHandler;
|
||||
public Action<IEntity, GridCoordinates> FireHandler;
|
||||
|
||||
public ServerRangedBarrelComponent Barrel
|
||||
{
|
||||
get => _barrel;
|
||||
set
|
||||
{
|
||||
if (_barrel != null && value != null)
|
||||
{
|
||||
Logger.Error("Tried setting Barrel on RangedWeapon that already has one");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_barrel = value;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
private ServerRangedBarrelComponent _barrel;
|
||||
|
||||
private FireRateSelector FireRateSelector => _barrel?.FireRateSelector ?? FireRateSelector.Safety;
|
||||
|
||||
private bool WeaponCanFire()
|
||||
{
|
||||
@@ -32,12 +52,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
return (UserCanFireHandler == null || UserCanFireHandler(user)) && ActionBlockerSystem.CanAttack(user);
|
||||
}
|
||||
|
||||
private void Fire(IEntity user, GridCoordinates clickLocation)
|
||||
{
|
||||
_lastFireTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
FireHandler?.Invoke(user, clickLocation);
|
||||
}
|
||||
|
||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession session = null)
|
||||
{
|
||||
base.HandleNetworkMessage(message, channel, session);
|
||||
@@ -49,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case SyncFirePosMessage msg:
|
||||
case FirePosComponentMessage msg:
|
||||
var user = session.AttachedEntity;
|
||||
if (user == null)
|
||||
{
|
||||
@@ -61,16 +75,9 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
}
|
||||
}
|
||||
|
||||
// Probably shouldn't be a separate method but don't want anything except NPCs calling this,
|
||||
// and currently ranged combat is handled via player only messages
|
||||
public void AiFire(IEntity entity, GridCoordinates coordinates)
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
if (!entity.HasComponent<AiControllerComponent>())
|
||||
{
|
||||
throw new InvalidOperationException("Only AIs should call AiFire");
|
||||
}
|
||||
|
||||
_tryFire(entity, coordinates);
|
||||
return new RangedWeaponComponentState(FireRateSelector);
|
||||
}
|
||||
|
||||
private void _tryFire(IEntity user, GridCoordinates coordinates)
|
||||
@@ -91,12 +98,19 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
|
||||
|
||||
var curTime = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
var span = curTime - _lastFireTime;
|
||||
if (span.TotalSeconds < 1 / FireRate)
|
||||
if (span.TotalSeconds < 1 / _barrel.FireRate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Fire(user, coordinates);
|
||||
_lastFireTime = curTime;
|
||||
FireHandler?.Invoke(user, coordinates);
|
||||
}
|
||||
|
||||
// Probably a better way to do this.
|
||||
void IHandSelected.HandSelected(HandSelectedEventArgs eventArgs)
|
||||
{
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Weapons;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Physics;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Weapon
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ServerFlashableComponent : SharedFlashableComponent
|
||||
{
|
||||
private double _duration;
|
||||
private TimeSpan _lastFlash;
|
||||
|
||||
public void Flash(double duration)
|
||||
{
|
||||
var timing = IoCManager.Resolve<IGameTiming>();
|
||||
_lastFlash = timing.CurTime;
|
||||
_duration = duration;
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new FlashComponentState(_duration, _lastFlash);
|
||||
}
|
||||
|
||||
public static void FlashAreaHelper(IEntity source, double range, double duration, string sound = null)
|
||||
{
|
||||
var physicsManager = IoCManager.Resolve<IPhysicsManager>();
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
foreach (var entity in entityManager.GetEntities(new TypeEntityQuery(typeof(ServerFlashableComponent))))
|
||||
{
|
||||
if (source.Transform.MapID != entity.Transform.MapID ||
|
||||
entity == source)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var direction = entity.Transform.WorldPosition - source.Transform.WorldPosition;
|
||||
|
||||
if (direction.Length > range)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Direction will be zero if they're hit with the source only I think
|
||||
if (direction == Vector2.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var ray = new CollisionRay(source.Transform.WorldPosition, direction.Normalized, (int) CollisionGroup.Opaque);
|
||||
var rayCastResults = physicsManager.IntersectRay(source.Transform.MapID, ray, direction.Length, source, false).ToList();
|
||||
if (rayCastResults.Count == 0 ||
|
||||
rayCastResults[0].HitEntity != entity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var flashable = entity.GetComponent<ServerFlashableComponent>();
|
||||
flashable.Flash(duration);
|
||||
}
|
||||
|
||||
if (sound != null)
|
||||
{
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().PlayAtCoords(sound, source.Transform.GridPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,4 +23,4 @@ namespace Content.Server.GameObjects.EntitySystems
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,5 +29,6 @@
|
||||
<Folder Include="GameObjects\Components\Construction\" />
|
||||
<Folder Include="GameObjects\Components\Trigger\" />
|
||||
<EmbeddedResource Include="Text\Names\*.txt" />
|
||||
<Folder Include="GameObjects\Components\Visualizers" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public class BallisticMagazineWeaponComponentState : ComponentState
|
||||
{
|
||||
/// <summary>
|
||||
/// True if a bullet is chambered.
|
||||
/// </summary>
|
||||
public bool Chambered { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Count of bullets in the magazine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if no magazine is inserted.
|
||||
/// </remarks>
|
||||
public (int count, int max)? MagazineCount { get; }
|
||||
|
||||
public BallisticMagazineWeaponComponentState(bool chambered, (int count, int max)? magazineCount) : base(ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON)
|
||||
{
|
||||
Chambered = chambered;
|
||||
MagazineCount = magazineCount;
|
||||
}
|
||||
}
|
||||
|
||||
// BMW is "Ballistic Magazine Weapon" here.
|
||||
/// <summary>
|
||||
/// Fired server -> client when the magazine in a Ballistic Magazine Weapon got auto-ejected.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BmwComponentAutoEjectedMessage : ComponentMessage
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Weapons.Ranged.Barrels
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum AmmoVisuals
|
||||
{
|
||||
AmmoCount,
|
||||
AmmoMax,
|
||||
Spent,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum MagazineBarrelVisuals
|
||||
{
|
||||
MagLoaded
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum BarrelBoltVisuals
|
||||
{
|
||||
BoltOpen,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public 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) :
|
||||
base(ContentNetIDs.MAGAZINE_BARREL)
|
||||
{
|
||||
Chambered = chambered;
|
||||
FireRateSelector = fireRateSelector;
|
||||
Magazine = magazine;
|
||||
SoundGunshot = soundGunshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum BallisticMagazineVisuals
|
||||
{
|
||||
AmmoCapacity,
|
||||
AmmoLeft,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum BallisticMagazineWeaponVisuals
|
||||
{
|
||||
MagazineLoaded,
|
||||
AmmoCapacity,
|
||||
AmmoLeft,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
public abstract class SharedRangedBarrelComponent : Component
|
||||
{
|
||||
public abstract FireRateSelector FireRateSelector { get; }
|
||||
public abstract FireRateSelector AllRateSelectors { get; }
|
||||
public abstract float FireRate { get; }
|
||||
public abstract int ShotsLeft { get; }
|
||||
public abstract int Capacity { get; }
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum FireRateSelector
|
||||
{
|
||||
Safety = 0,
|
||||
Single = 1 << 0,
|
||||
Automatic = 1 << 1,
|
||||
}
|
||||
}
|
||||
@@ -5,40 +5,35 @@ using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Weapons.Ranged
|
||||
{
|
||||
public class SharedRangedWeaponComponent : Component
|
||||
public abstract class SharedRangedWeaponComponent : Component
|
||||
{
|
||||
private float _fireRate;
|
||||
private bool _automatic;
|
||||
// Each RangedWeapon should have a RangedWeapon component +
|
||||
// some kind of RangedBarrelComponent (this dictates what ammo is retrieved).
|
||||
public override string Name => "RangedWeapon";
|
||||
public override uint? NetID => ContentNetIDs.RANGED_WEAPON;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, this weapon is fully automatic, holding down left mouse button will keep firing it.
|
||||
/// </summary>
|
||||
public bool Automatic => _automatic;
|
||||
|
||||
/// <summary>
|
||||
/// If the weapon is automatic, controls how many shots can be fired per second.
|
||||
/// </summary>
|
||||
public float FireRate => _fireRate;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RangedWeaponComponentState : ComponentState
|
||||
{
|
||||
public FireRateSelector FireRateSelector { get; }
|
||||
|
||||
public RangedWeaponComponentState(
|
||||
FireRateSelector fireRateSelector
|
||||
) : base(ContentNetIDs.RANGED_WEAPON)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _fireRate, "firerate", 4);
|
||||
serializer.DataField(ref _automatic, "automatic", false);
|
||||
FireRateSelector = fireRateSelector;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
protected class SyncFirePosMessage : ComponentMessage
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class FirePosComponentMessage : ComponentMessage
|
||||
{
|
||||
public GridCoordinates Target { get; }
|
||||
|
||||
public FirePosComponentMessage(GridCoordinates target)
|
||||
{
|
||||
public readonly GridCoordinates Target;
|
||||
|
||||
public SyncFirePosMessage(GridCoordinates target)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Weapons
|
||||
{
|
||||
public class SharedFlashableComponent : Component
|
||||
{
|
||||
public override string Name => "Flashable";
|
||||
public override uint? NetID => ContentNetIDs.FLASHABLE;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class FlashComponentState : ComponentState
|
||||
{
|
||||
public double Duration { get; }
|
||||
public TimeSpan Time { get; }
|
||||
|
||||
public FlashComponentState(double duration, TimeSpan time) : base(ContentNetIDs.FLASHABLE)
|
||||
{
|
||||
Duration = duration;
|
||||
Time = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
public const uint DAMAGEABLE = 1000;
|
||||
public const uint DESTRUCTIBLE = 1001;
|
||||
public const uint BALLISTIC_MAGAZINE_WEAPON = 1002;
|
||||
public const uint MAGAZINE_BARREL = 1002;
|
||||
public const uint HANDS = 1003;
|
||||
public const uint SOLUTION = 1004;
|
||||
public const uint STORAGE = 1005;
|
||||
@@ -50,7 +50,8 @@
|
||||
public const uint PDA = 1044;
|
||||
public const uint PATHFINDER_DEBUG = 1045;
|
||||
public const uint AI_DEBUG = 1046;
|
||||
|
||||
public const uint FLASHABLE = 1047;
|
||||
|
||||
// Net IDs for integration tests.
|
||||
public const uint PREDICTION_TEST = 10001;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace Content.Shared.Utility
|
||||
}
|
||||
|
||||
var preround = toOne * (levels - 1);
|
||||
if (toOne <= threshold || levels == 2)
|
||||
if (toOne <= threshold || levels <= 2)
|
||||
{
|
||||
return (int)Math.Ceiling(preround);
|
||||
}
|
||||
|
||||
BIN
Resources/Audio/Guns/Bolt/lmg_bolt_closed.ogg
Normal file
BIN
Resources/Audio/Guns/Bolt/lmg_bolt_closed.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Guns/Bolt/lmg_bolt_open.ogg
Normal file
BIN
Resources/Audio/Guns/Bolt/lmg_bolt_open.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Guns/Bolt/rifle_bolt_closed.ogg
Normal file
BIN
Resources/Audio/Guns/Bolt/rifle_bolt_closed.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Guns/Bolt/rifle_bolt_open.ogg
Normal file
BIN
Resources/Audio/Guns/Bolt/rifle_bolt_open.ogg
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user