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:
metalgearsloth
2020-06-22 05:47:15 +10:00
committed by GitHub
parent ac19ad7eac
commit 95995b6232
1977 changed files with 13600 additions and 11229 deletions

View File

@@ -85,8 +85,6 @@ namespace Content.Client
"Multitool", "Multitool",
"Wrench", "Wrench",
"Crowbar", "Crowbar",
"HitscanWeapon",
"ProjectileWeapon",
"Projectile", "Projectile",
"MeleeWeapon", "MeleeWeapon",
"Storeable", "Storeable",
@@ -100,8 +98,8 @@ namespace Content.Client
"LightBulb", "LightBulb",
"Healing", "Healing",
"Catwalk", "Catwalk",
"BallisticMagazine", "RangedMagazine",
"BallisticBullet", "Ammo",
"HitscanWeaponCapacitor", "HitscanWeaponCapacitor",
"PowerCell", "PowerCell",
"WeaponCapacitorCharger", "WeaponCapacitorCharger",
@@ -148,6 +146,13 @@ namespace Content.Client
"Bucket", "Bucket",
"Puddle", "Puddle",
"CanSpill", "CanSpill",
"SpeedLoader",
"Hitscan",
"BoltActionBarrel",
"PumpBarrel",
"RevolverBarrel",
"ExplosiveProjectile",
"StunnableProjectile",
"RandomPottedPlant", "RandomPottedPlant",
"CommunicationsConsole", "CommunicationsConsole",
"BarSign", "BarSign",
@@ -167,6 +172,9 @@ namespace Content.Client
"SecureEntityStorage", "SecureEntityStorage",
"PresetIdCard", "PresetIdCard",
"SolarControlConsole", "SolarControlConsole",
"BatteryBarrel",
"FlashExplosive",
"FlashProjectile",
"Utensil", "Utensil",
}; };

View File

@@ -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;
}
}
}

View File

@@ -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}");
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -1,28 +1,25 @@
using System; using System;
using Content.Client.Animations; using Content.Client.Animations;
using Content.Client.UserInterface;
using Content.Client.UserInterface.Stylesheets; using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility; using Content.Client.Utility;
using Content.Shared.GameObjects; 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.Animations;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Animations; using Robust.Shared.Animations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Network;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Players; using Robust.Shared.Players;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables; 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] [RegisterComponent]
public class BallisticMagazineWeaponComponent : Component, IItemStatus public class ClientMagazineBarrelComponent : Component, IItemStatus
{ {
private static readonly Animation AlarmAnimationSmg = new Animation 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 string Name => "MagazineBarrel";
public override uint? NetID => ContentNetIDs.BALLISTIC_MAGAZINE_WEAPON; public override uint? NetID => ContentNetIDs.MAGAZINE_BARREL;
private StatusControl _statusControl; private StatusControl _statusControl;
@@ -101,11 +98,11 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
public override void HandleComponentState(ComponentState curState, ComponentState nextState) public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{ {
if (!(curState is BallisticMagazineWeaponComponentState cast)) if (!(curState is MagazineBarrelComponentState cast))
return; return;
Chambered = cast.Chambered; Chambered = cast.Chambered;
MagazineCount = cast.MagazineCount; MagazineCount = cast.Magazine;
_statusControl?.Update(); _statusControl?.Update();
} }
@@ -115,9 +112,10 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
switch (message) switch (message)
{ {
/*
case BmwComponentAutoEjectedMessage _: case BmwComponentAutoEjectedMessage _:
_statusControl?.PlayAlarmAnimation(); _statusControl?.PlayAlarmAnimation();
return; return;*/
} }
} }
@@ -138,13 +136,13 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
private sealed class StatusControl : Control private sealed class StatusControl : Control
{ {
private readonly BallisticMagazineWeaponComponent _parent; private readonly ClientMagazineBarrelComponent _parent;
private readonly HBoxContainer _bulletsListTop; private readonly HBoxContainer _bulletsListTop;
private readonly HBoxContainer _bulletsListBottom; private readonly HBoxContainer _bulletsListBottom;
private readonly TextureRect _chamberedBullet; private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel; private readonly Label _noMagazineLabel;
public StatusControl(BallisticMagazineWeaponComponent parent) public StatusControl(ClientMagazineBarrelComponent parent)
{ {
_parent = parent; _parent = parent;
SizeFlagsHorizontal = SizeFlags.FillExpand; SizeFlagsHorizontal = SizeFlags.FillExpand;
@@ -181,7 +179,7 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
}, },
(_chamberedBullet = new TextureRect (_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, SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Fill, SizeFlagsHorizontal = SizeFlags.ShrinkEnd | SizeFlags.Fill,
}) })
@@ -223,7 +221,7 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
texturePath = "/Textures/UserInterface/status/bullets/tiny.png"; texturePath = "/Textures/UserInterface/status/bullets/tiny.png";
} }
var texture = ResC.GetTexture(texturePath); var texture = StaticIoC.ResC.GetTexture(texturePath);
const int tinyMaxRow = 60; const int tinyMaxRow = 60;
@@ -282,4 +280,4 @@ namespace Content.Client.GameObjects.Components.Weapons.Ranged
} }
} }
} }
} }

View File

@@ -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");
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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,
}
}

View File

@@ -4,12 +4,35 @@ using Robust.Shared.Map;
namespace Content.Client.GameObjects.Components.Weapons.Ranged 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] [RegisterComponent]
public sealed class ClientRangedWeaponComponent : SharedRangedWeaponComponent 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) public void SyncFirePos(GridCoordinates worldPos)
{ {
SendNetworkMessage(new SyncFirePosMessage(worldPos)); SendNetworkMessage(new FirePosComponentMessage(worldPos));
} }
} }
} }

View File

@@ -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}");
}
}
}
}

View File

@@ -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.Client.Interfaces.GameObjects;
using Content.Shared.GameObjects.Components.Weapons.Ranged;
using Robust.Client.GameObjects.EntitySystems; using Robust.Client.GameObjects.EntitySystems;
using Robust.Client.Interfaces.Graphics.ClientEye; using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.Input; using Robust.Client.Interfaces.Input;
@@ -26,8 +28,8 @@ namespace Content.Client.GameObjects.EntitySystems
private InputSystem _inputSystem; private InputSystem _inputSystem;
private CombatModeSystem _combatModeSystem; private CombatModeSystem _combatModeSystem;
private bool _isFirstShot;
private bool _blocked; private bool _blocked;
private int _shotCounter;
public override void Initialize() public override void Initialize()
{ {
@@ -46,18 +48,15 @@ namespace Content.Client.GameObjects.EntitySystems
{ {
return; return;
} }
var canFireSemi = _isFirstShot;
var state = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use); var state = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use);
if (!_combatModeSystem.IsInCombatMode() || state != BoundKeyState.Down) if (!_combatModeSystem.IsInCombatMode() || state != BoundKeyState.Down)
{ {
_isFirstShot = true; _shotCounter = 0;
_blocked = false; _blocked = false;
return; return;
} }
_isFirstShot = false;
var entity = _playerManager.LocalPlayer.ControlledEntity; var entity = _playerManager.LocalPlayer.ControlledEntity;
if (entity == null || !entity.TryGetComponent(out IHandsComponent hands)) if (entity == null || !entity.TryGetComponent(out IHandsComponent hands))
{ {
@@ -71,6 +70,25 @@ namespace Content.Client.GameObjects.EntitySystems
return; 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) if (_blocked)
{ {
return; return;
@@ -81,10 +99,7 @@ namespace Content.Client.GameObjects.EntitySystems
if (!_mapManager.TryFindGridAt(worldPos, out var grid)) if (!_mapManager.TryFindGridAt(worldPos, out var grid))
grid = _mapManager.GetDefaultGrid(worldPos.MapId); grid = _mapManager.GetDefaultGrid(worldPos.MapId);
if (weapon.Automatic || canFireSemi) weapon.SyncFirePos(grid.MapToGrid(worldPos));
{
weapon.SyncFirePos(grid.MapToGrid(worldPos));
}
} }
} }
} }

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -3,7 +3,6 @@ using Content.Server.AI.Operators;
using Content.Server.AI.Operators.Inventory; using Content.Server.AI.Operators.Inventory;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat.Melee; 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.Considerations.Inventory;
using Content.Server.AI.Utility.Curves; using Content.Server.AI.Utility.Curves;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
@@ -41,9 +40,6 @@ namespace Content.Server.AI.Utility.Actions.Combat.Melee
protected override Consideration[] Considerations { get; } = { protected override Consideration[] Considerations { get; } = {
new MeleeWeaponEquippedCon( new MeleeWeaponEquippedCon(
new InverseBoolCurve()), 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 CanPutTargetInHandsCon(
new BoolCurve()), new BoolCurve()),
new MeleeWeaponSpeedCon( new MeleeWeaponSpeedCon(

View File

@@ -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);
}
}
}
}

View File

@@ -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()),
};
}
}

View File

@@ -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)),
};
}
}

View File

@@ -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)),
};
}
}

View File

@@ -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
};
}
}

View File

@@ -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)),
};
}
}

View File

@@ -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()),
};
}
}

View File

@@ -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)),
};
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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)),
};
}
}

View File

@@ -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)),
};
}
}

View File

@@ -1,13 +1,5 @@
using System.Collections.Generic;
using Content.Server.AI.Utility.Actions; 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.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; using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.AI.Utility.BehaviorSets namespace Content.Server.AI.Utility.BehaviorSets
@@ -18,17 +10,9 @@ namespace Content.Server.AI.Utility.BehaviorSets
{ {
Actions = new IAiUtility[] Actions = new IAiUtility[]
{ {
new PickUpRangedExp(),
// TODO: Reload Ballistic // TODO: Reload Ballistic
new DropEmptyBallisticExp(),
// TODO: Ideally long-term we should just store the weapons in backpack // TODO: Ideally long-term we should just store the weapons in backpack
new DropEmptyHitscanExp(),
new EquipMeleeExp(), new EquipMeleeExp(),
new EquipBallisticExp(),
new EquipHitscanExp(),
new PickUpHitscanFromChargersExp(),
new ChargeEquippedHitscanExp(),
new RangedAttackNearbySpeciesExp(),
new PickUpMeleeWeaponExp(), new PickUpMeleeWeaponExp(),
new MeleeAttackNearbySpeciesExp(), new MeleeAttackNearbySpeciesExp(),
}; };

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
using Content.Shared.GameObjects.Components.Power; using Content.Shared.GameObjects.Components.Power;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container; using Robust.Server.GameObjects.Components.Container;
@@ -13,7 +14,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
public abstract class BaseCharger : Component public abstract class BaseCharger : Component
{ {
public IEntity HeldItem { get; protected set; } protected IEntity _heldItem;
protected ContainerSlot _container; protected ContainerSlot _container;
protected PowerDeviceComponent _powerDevice; protected PowerDeviceComponent _powerDevice;
public CellChargerStatus Status => _status; public CellChargerStatus Status => _status;
@@ -58,37 +59,28 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
} }
/// <summary> /// <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> /// </summary>
/// <param name="user"></param> /// <param name="user"></param>
public void RemoveItemToHand(IEntity user) public void RemoveItem(IEntity user)
{ {
var heldItem = _container.ContainedEntity; var heldItem = _container.ContainedEntity;
if (heldItem == null) if (heldItem == null)
{ {
return; return;
} }
RemoveItem();
if (user.TryGetComponent(out HandsComponent handsComponent) && _container.Remove(heldItem);
heldItem.TryGetComponent(out ItemComponent itemComponent)) if (user.TryGetComponent(out HandsComponent handsComponent))
{ {
handsComponent.PutInHand(itemComponent); handsComponent.PutInHandOrDrop(heldItem.GetComponent<ItemComponent>());
}
}
/// <summary>
/// Will put the charger's item on the floor if available
/// </summary>
public void RemoveItem()
{
if (_container.ContainedEntity == null)
{
return;
} }
_container.Remove(HeldItem); if (heldItem.TryGetComponent(out ServerBatteryBarrelComponent batteryBarrelComponent))
HeldItem = null; {
batteryBarrelComponent.UpdateAppearance();
}
UpdateStatus(); UpdateStatus();
} }
@@ -135,8 +127,6 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
} }
_appearanceComponent?.SetData(CellVisual.Occupied, _container.ContainedEntity != null); _appearanceComponent?.SetData(CellVisual.Occupied, _container.ContainedEntity != null);
_status = status;
} }
public void OnUpdate(float frameTime) public void OnUpdate(float frameTime)

View File

@@ -53,7 +53,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
void IActivate.Activate(ActivateEventArgs eventArgs) void IActivate.Activate(ActivateEventArgs eventArgs)
{ {
RemoveItemToHand(eventArgs.User); RemoveItem(eventArgs.User);
} }
[Verb] [Verb]
@@ -111,7 +111,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
protected override void Activate(IEntity user, PowerCellChargerComponent component) 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; return false;
} }
HeldItem = entity; if (!_container.Insert(entity))
if (!_container.Insert(HeldItem))
{ {
return false; 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 // Two numbers: One for how much power actually goes into the device (chargeAmount) and
// chargeLoss which is how much is drawn from the powernet // 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; var chargeLoss = cellComponent.RequestCharge(frameTime) * _transferRatio;
_powerDevice.Load = chargeLoss; _powerDevice.Load = chargeLoss;

View File

@@ -1,18 +1,13 @@
using System; 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.GameObjects.EntitySystems;
using Content.Server.Utility;
using Content.Shared.GameObjects; using Content.Shared.GameObjects;
using Content.Shared.GameObjects.Components.Power; using Content.Shared.GameObjects.Components.Power;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Robust.Server.GameObjects;
using Robust.Server.GameObjects.Components.Container;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Power.Chargers 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 string Name => "WeaponCapacitorCharger";
public override double CellChargePercent => _container.ContainedEntity != null ? public override double CellChargePercent => _container.ContainedEntity != null ?
_container.ContainedEntity.GetComponent<HitscanWeaponCapacitorComponent>().Charge / _container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell.Charge /
_container.ContainedEntity.GetComponent<HitscanWeaponCapacitorComponent>().Capacity * 100 : 0.0f; _container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell.Capacity * 100 : 0.0f;
bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) bool IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{ {
@@ -43,7 +38,7 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
void IActivate.Activate(ActivateEventArgs eventArgs) void IActivate.Activate(ActivateEventArgs eventArgs)
{ {
RemoveItemToHand(eventArgs.User); RemoveItem(eventArgs.User);
} }
[Verb] [Verb]
@@ -106,21 +101,19 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
protected override void Activate(IEntity user, WeaponCapacitorChargerComponent component) protected override void Activate(IEntity user, WeaponCapacitorChargerComponent component)
{ {
component.RemoveItem(); component.RemoveItem(user);
} }
} }
public bool TryInsertItem(IEntity entity) public bool TryInsertItem(IEntity entity)
{ {
if (!entity.HasComponent<HitscanWeaponCapacitorComponent>() || if (!entity.HasComponent<ServerBatteryBarrelComponent>() ||
_container.ContainedEntity != null) _container.ContainedEntity != null)
{ {
return false; return false;
} }
HeldItem = entity; if (!_container.Insert(entity))
if (!_container.Insert(HeldItem))
{ {
return false; return false;
} }
@@ -140,8 +133,8 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
return CellChargerStatus.Empty; return CellChargerStatus.Empty;
} }
if (_container.ContainedEntity.TryGetComponent(out HitscanWeaponCapacitorComponent component) && if (_container.ContainedEntity.TryGetComponent(out ServerBatteryBarrelComponent component) &&
Math.Abs(component.Capacity - component.Charge) < 0.01) Math.Abs(component.PowerCell.Capacity - component.PowerCell.Charge) < 0.01)
{ {
return CellChargerStatus.Charged; 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 // Two numbers: One for how much power actually goes into the device (chargeAmount) and
// chargeLoss which is how much is drawn from the powernet // chargeLoss which is how much is drawn from the powernet
_container.ContainedEntity.TryGetComponent(out HitscanWeaponCapacitorComponent weaponCapacitorComponent); var powerCell = _container.ContainedEntity.GetComponent<ServerBatteryBarrelComponent>().PowerCell;
var chargeLoss = weaponCapacitorComponent.RequestCharge(frameTime) * _transferRatio; var chargeLoss = powerCell.RequestCharge(frameTime) * _transferRatio;
_powerDevice.Load = chargeLoss; _powerDevice.Load = chargeLoss;
if (!_powerDevice.Powered) if (!_powerDevice.Powered)
@@ -165,14 +158,13 @@ namespace Content.Server.GameObjects.Components.Power.Chargers
var chargeAmount = chargeLoss * _transferEfficiency; var chargeAmount = chargeLoss * _transferEfficiency;
weaponCapacitorComponent.AddCharge(chargeAmount); powerCell.AddCharge(chargeAmount);
// Just so the sprite won't be set to 99.99999% visibility // Just so the sprite won't be set to 99.99999% visibility
if (weaponCapacitorComponent.Capacity - weaponCapacitorComponent.Charge < 0.01) if (powerCell.Capacity - powerCell.Charge < 0.01)
{ {
weaponCapacitorComponent.Charge = weaponCapacitorComponent.Capacity; powerCell.Charge = powerCell.Capacity;
} }
UpdateStatus(); UpdateStatus();
} }
} }
} }

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -2,9 +2,13 @@
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.GameObjects; using Content.Shared.GameObjects;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -18,24 +22,32 @@ namespace Content.Server.GameObjects.Components.Projectiles
public bool IgnoreShooter = true; public bool IgnoreShooter = true;
private EntityUid Shooter = EntityUid.Invalid; private EntityUid _shooter = EntityUid.Invalid;
private Dictionary<DamageType, int> _damages; private Dictionary<DamageType, int> _damages;
[ViewVariables] [ViewVariables]
public Dictionary<DamageType, int> Damages => _damages; public Dictionary<DamageType, int> Damages
private float _velocity;
public float Velocity
{ {
get => _velocity; get => _damages;
set => _velocity = value; 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) public override void ExposeData(ObjectSerializer serializer)
{ {
base.ExposeData(serializer); base.ExposeData(serializer);
serializer.DataField(ref _deleteOnCollide, "delete_on_collide", true);
// If not specified 0 damage // If not specified 0 damage
serializer.DataField(ref _damages, "damages", new Dictionary<DamageType, int>()); 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; public float TimeLeft { get; set; } = 10;
@@ -46,7 +58,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
/// <param name="shooter"></param> /// <param name="shooter"></param>
public void IgnoreEntity(IEntity shooter) public void IgnoreEntity(IEntity shooter)
{ {
Shooter = shooter.Uid; _shooter = shooter.Uid;
} }
/// <summary> /// <summary>
@@ -56,7 +68,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
/// <returns></returns> /// <returns></returns>
bool ICollideSpecial.PreventCollide(IPhysBody collidedwith) bool ICollideSpecial.PreventCollide(IPhysBody collidedwith)
{ {
if (IgnoreShooter && collidedwith.Owner.Uid == Shooter) if (IgnoreShooter && collidedwith.Owner.Uid == _shooter)
return true; return true;
return false; return false;
} }
@@ -67,9 +79,17 @@ namespace Content.Server.GameObjects.Components.Projectiles
/// <param name="entity"></param> /// <param name="entity"></param>
void ICollideBehavior.CollideWith(IEntity entity) 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)) 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) foreach (var (damageType, amount) in _damages)
{ {
@@ -87,7 +107,7 @@ namespace Content.Server.GameObjects.Components.Projectiles
void ICollideBehavior.PostCollide(int collideCount) void ICollideBehavior.PostCollide(int collideCount)
{ {
if (collideCount > 0) Owner.Delete(); if (collideCount > 0 && DeleteOnCollide) Owner.Delete();
} }
} }
} }

View File

@@ -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) {}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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,
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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"));
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,26 +1,46 @@
using System; using System;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Weapons.Ranged; using Content.Shared.GameObjects.Components.Weapons.Ranged;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Network;
using Robust.Shared.Interfaces.Timing; using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Players; using Robust.Shared.Players;
namespace Content.Server.GameObjects.Components.Weapon.Ranged namespace Content.Server.GameObjects.Components.Weapon.Ranged
{ {
[RegisterComponent] [RegisterComponent]
public sealed class RangedWeaponComponent : SharedRangedWeaponComponent public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent, IHandSelected
{ {
private TimeSpan _lastFireTime; private TimeSpan _lastFireTime;
public Func<bool> WeaponCanFireHandler; public Func<bool> WeaponCanFireHandler;
public Func<IEntity, bool> UserCanFireHandler; public Func<IEntity, bool> UserCanFireHandler;
public Action<IEntity, GridCoordinates> FireHandler; 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() private bool WeaponCanFire()
{ {
@@ -32,12 +52,6 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
return (UserCanFireHandler == null || UserCanFireHandler(user)) && ActionBlockerSystem.CanAttack(user); 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) public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession session = null)
{ {
base.HandleNetworkMessage(message, channel, session); base.HandleNetworkMessage(message, channel, session);
@@ -49,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged
switch (message) switch (message)
{ {
case SyncFirePosMessage msg: case FirePosComponentMessage msg:
var user = session.AttachedEntity; var user = session.AttachedEntity;
if (user == null) 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, public override ComponentState GetComponentState()
// and currently ranged combat is handled via player only messages
public void AiFire(IEntity entity, GridCoordinates coordinates)
{ {
if (!entity.HasComponent<AiControllerComponent>()) return new RangedWeaponComponentState(FireRateSelector);
{
throw new InvalidOperationException("Only AIs should call AiFire");
}
_tryFire(entity, coordinates);
} }
private void _tryFire(IEntity user, GridCoordinates coordinates) 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 curTime = IoCManager.Resolve<IGameTiming>().CurTime;
var span = curTime - _lastFireTime; var span = curTime - _lastFireTime;
if (span.TotalSeconds < 1 / FireRate) if (span.TotalSeconds < 1 / _barrel.FireRate)
{ {
return; return;
} }
Fire(user, coordinates); _lastFireTime = curTime;
FireHandler?.Invoke(user, coordinates);
}
// Probably a better way to do this.
void IHandSelected.HandSelected(HandSelectedEventArgs eventArgs)
{
Dirty();
} }
} }
} }

View File

@@ -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);
}
}
}
}

View File

@@ -23,4 +23,4 @@ namespace Content.Server.GameObjects.EntitySystems
} }
} }
} }
} }

View File

@@ -29,5 +29,6 @@
<Folder Include="GameObjects\Components\Construction\" /> <Folder Include="GameObjects\Components\Construction\" />
<Folder Include="GameObjects\Components\Trigger\" /> <Folder Include="GameObjects\Components\Trigger\" />
<EmbeddedResource Include="Text\Names\*.txt" /> <EmbeddedResource Include="Text\Names\*.txt" />
<Folder Include="GameObjects\Components\Visualizers" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -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
{
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,12 +0,0 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Weapons.Ranged
{
[Serializable, NetSerializable]
public enum BallisticMagazineVisuals
{
AmmoCapacity,
AmmoLeft,
}
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -5,40 +5,35 @@ using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Weapons.Ranged namespace Content.Shared.GameObjects.Components.Weapons.Ranged
{ {
public class SharedRangedWeaponComponent : Component public abstract class SharedRangedWeaponComponent : Component
{ {
private float _fireRate; // Each RangedWeapon should have a RangedWeapon component +
private bool _automatic; // some kind of RangedBarrelComponent (this dictates what ammo is retrieved).
public override string Name => "RangedWeapon"; public override string Name => "RangedWeapon";
public override uint? NetID => ContentNetIDs.RANGED_WEAPON; public override uint? NetID => ContentNetIDs.RANGED_WEAPON;
}
/// <summary> [Serializable, NetSerializable]
/// If true, this weapon is fully automatic, holding down left mouse button will keep firing it. public sealed class RangedWeaponComponentState : ComponentState
/// </summary> {
public bool Automatic => _automatic; public FireRateSelector FireRateSelector { get; }
/// <summary> public RangedWeaponComponentState(
/// If the weapon is automatic, controls how many shots can be fired per second. FireRateSelector fireRateSelector
/// </summary> ) : base(ContentNetIDs.RANGED_WEAPON)
public float FireRate => _fireRate;
public override void ExposeData(ObjectSerializer serializer)
{ {
base.ExposeData(serializer); FireRateSelector = fireRateSelector;
serializer.DataField(ref _fireRate, "firerate", 4);
serializer.DataField(ref _automatic, "automatic", false);
} }
}
[Serializable, NetSerializable] [Serializable, NetSerializable]
protected class SyncFirePosMessage : ComponentMessage public sealed class FirePosComponentMessage : ComponentMessage
{
public GridCoordinates Target { get; }
public FirePosComponentMessage(GridCoordinates target)
{ {
public readonly GridCoordinates Target; Target = target;
public SyncFirePosMessage(GridCoordinates target)
{
Target = target;
}
} }
} }
} }

View File

@@ -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;
}
}
}

View File

@@ -5,7 +5,7 @@
{ {
public const uint DAMAGEABLE = 1000; public const uint DAMAGEABLE = 1000;
public const uint DESTRUCTIBLE = 1001; 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 HANDS = 1003;
public const uint SOLUTION = 1004; public const uint SOLUTION = 1004;
public const uint STORAGE = 1005; public const uint STORAGE = 1005;
@@ -50,7 +50,8 @@
public const uint PDA = 1044; public const uint PDA = 1044;
public const uint PATHFINDER_DEBUG = 1045; public const uint PATHFINDER_DEBUG = 1045;
public const uint AI_DEBUG = 1046; public const uint AI_DEBUG = 1046;
public const uint FLASHABLE = 1047;
// Net IDs for integration tests. // Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001; public const uint PREDICTION_TEST = 10001;
} }

View File

@@ -47,7 +47,7 @@ namespace Content.Shared.Utility
} }
var preround = toOne * (levels - 1); var preround = toOne * (levels - 1);
if (toOne <= threshold || levels == 2) if (toOne <= threshold || levels <= 2)
{ {
return (int)Math.Ceiling(preround); return (int)Math.Ceiling(preround);
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More