Melee refactor (#10897)

Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-09-29 15:51:59 +10:00
committed by GitHub
parent c583b7b361
commit f51248ecaa
140 changed files with 2440 additions and 1824 deletions

View File

@@ -0,0 +1,49 @@
using System.Linq;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Weapons.Ranged.Events;
namespace Content.Server.Weapons.Ranged.Systems
{
public sealed class ChemicalAmmoSystem : EntitySystem
{
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<ChemicalAmmoComponent, AmmoShotEvent>(OnFire);
}
private void OnFire(EntityUid uid, ChemicalAmmoComponent component, AmmoShotEvent args)
{
if (!_solutionSystem.TryGetSolution(uid, component.SolutionName, out var ammoSolution))
return;
var projectiles = args.FiredProjectiles;
var projectileSolutionContainers = new List<(EntityUid, Solution)>();
foreach (var projectile in projectiles)
{
if (_solutionSystem
.TryGetSolution(projectile, component.SolutionName, out var projectileSolutionContainer))
{
projectileSolutionContainers.Add((uid, projectileSolutionContainer));
}
}
if (!projectileSolutionContainers.Any())
return;
var solutionPerProjectile = ammoSolution.CurrentVolume * (1 / projectileSolutionContainers.Count);
foreach (var (projectileUid, projectileSolution) in projectileSolutionContainers)
{
var solutionToTransfer = _solutionSystem.SplitSolution(uid, ammoSolution, solutionPerProjectile);
_solutionSystem.TryAddSolution(projectileUid, projectileSolution, solutionToTransfer);
}
_solutionSystem.RemoveAllSolution(uid, ammoSolution);
}
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Weapons.Ranged.Systems;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Map;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates)
{
EntityUid? ent = null;
// TODO: Combine with TakeAmmo
if (component.Entities.Count > 0)
{
var existing = component.Entities[^1];
component.Entities.RemoveAt(component.Entities.Count - 1);
component.Container.Remove(existing);
EnsureComp<AmmoComponent>(existing);
}
else if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
ent = Spawn(component.FillProto, coordinates);
EnsureComp<AmmoComponent>(ent.Value);
}
if (ent != null)
EjectCartridge(ent.Value);
}
}

View File

@@ -0,0 +1,133 @@
using Content.Server.Power.Components;
using Content.Server.Projectiles.Components;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, GetVerbsEvent<ExamineVerb>>(OnBatteryExaminableVerb);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, GetVerbsEvent<ExamineVerb>>(OnBatteryExaminableVerb);
}
private void OnBatteryStartup(EntityUid uid, BatteryAmmoProviderComponent component, ComponentStartup args)
{
UpdateShots(uid, component);
}
private void OnBatteryChargeChange(EntityUid uid, BatteryAmmoProviderComponent component, ChargeChangedEvent args)
{
UpdateShots(uid, component);
}
private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component)
{
if (!TryComp<BatteryComponent>(uid, out var battery)) return;
UpdateShots(component, battery);
}
private void UpdateShots(BatteryAmmoProviderComponent component, BatteryComponent battery)
{
var shots = (int) (battery.CurrentCharge / component.FireCost);
var maxShots = (int) (battery.MaxCharge / component.FireCost);
if (component.Shots != shots || component.Capacity != maxShots)
{
Dirty(component);
}
component.Shots = shots;
component.Capacity = maxShots;
UpdateBatteryAppearance(component.Owner, component);
}
private void OnBatteryExaminableVerb(EntityUid uid, BatteryAmmoProviderComponent component, GetVerbsEvent<ExamineVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;
var damageSpec = GetDamage(component);
if (damageSpec == null)
return;
string damageType;
switch (component)
{
case HitscanBatteryAmmoProviderComponent:
damageType = Loc.GetString("damage-hitscan");
break;
case ProjectileBatteryAmmoProviderComponent:
damageType = Loc.GetString("damage-projectile");
break;
default:
throw new ArgumentOutOfRangeException();
}
var verb = new ExamineVerb()
{
Act = () =>
{
var markup = Damageable.GetDamageExamine(damageSpec, damageType);
Examine.SendExamineTooltip(args.User, uid, markup, false, false);
},
Text = Loc.GetString("damage-examinable-verb-text"),
Message = Loc.GetString("damage-examinable-verb-message"),
Category = VerbCategory.Examine,
IconTexture = "/Textures/Interface/VerbIcons/smite.svg.192dpi.png"
};
args.Verbs.Add(verb);
}
private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component)
{
if (component is ProjectileBatteryAmmoProviderComponent battery)
{
if (ProtoManager.Index<EntityPrototype>(battery.Prototype).Components
.TryGetValue(_factory.GetComponentName(typeof(ProjectileComponent)), out var projectile))
{
var p = (ProjectileComponent) projectile.Component;
if (p.Damage.Total > FixedPoint2.Zero)
{
return p.Damage;
}
}
return null;
}
if (component is HitscanBatteryAmmoProviderComponent hitscan)
{
return ProtoManager.Index<HitscanPrototype>(hitscan.Prototype).Damage;
}
return null;
}
protected override void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component)
{
if (!TryComp<BatteryComponent>(uid, out var battery)) return;
battery.CurrentCharge -= component.FireCost;
UpdateShots(component, battery);
}
}

View File

@@ -0,0 +1,76 @@
using Content.Server.Projectiles.Components;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeCartridge()
{
base.InitializeCartridge();
SubscribeLocalEvent<CartridgeAmmoComponent, ExaminedEvent>(OnCartridgeExamine);
SubscribeLocalEvent<CartridgeAmmoComponent, GetVerbsEvent<ExamineVerb>>(OnCartridgeVerbExamine);
}
private void OnCartridgeVerbExamine(EntityUid uid, CartridgeAmmoComponent component, GetVerbsEvent<ExamineVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;
var damageSpec = GetProjectileDamage(component.Prototype);
if (damageSpec == null)
return;
var verb = new ExamineVerb()
{
Act = () =>
{
var markup = Damageable.GetDamageExamine(damageSpec, Loc.GetString("damage-projectile"));
_examine.SendExamineTooltip(args.User, uid, markup, false, false);
},
Text = Loc.GetString("damage-examinable-verb-text"),
Message = Loc.GetString("damage-examinable-verb-message"),
Category = VerbCategory.Examine,
IconTexture = "/Textures/Interface/VerbIcons/smite.svg.192dpi.png"
};
args.Verbs.Add(verb);
}
private DamageSpecifier? GetProjectileDamage(string proto)
{
if (!ProtoManager.TryIndex<EntityPrototype>(proto, out var entityProto))
return null;
if (entityProto.Components
.TryGetValue(_factory.GetComponentName(typeof(ProjectileComponent)), out var projectile))
{
var p = (ProjectileComponent) projectile.Component;
if (p.Damage.Total > FixedPoint2.Zero)
{
return p.Damage;
}
}
return null;
}
private void OnCartridgeExamine(EntityUid uid, CartridgeAmmoComponent component, ExaminedEvent args)
{
if (component.Spent)
{
args.PushMarkup(Loc.GetString("gun-cartridge-spent"));
}
else
{
args.PushMarkup(Loc.GetString("gun-cartridge-unspent"));
}
}
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void SpinRevolver(RevolverAmmoProviderComponent component, EntityUid? user = null)
{
base.SpinRevolver(component, user);
var index = Random.Next(component.Capacity);
if (component.CurrentIndex == index) return;
component.CurrentIndex = index;
Dirty(component);
}
}

View File

@@ -0,0 +1,389 @@
using System.Linq;
using Content.Server.Cargo.Systems;
using Content.Server.Damage.Systems;
using Content.Server.Examine;
using Content.Server.Interaction;
using Content.Server.Interaction.Components;
using Content.Server.Projectiles.Components;
using Content.Server.Stunnable;
using Content.Server.Weapons.Melee;
using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly ExamineSystem _examine = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;
[Dependency] private readonly StunSystem _stun = default!;
public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
public const float GunClumsyChance = 0.5f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BallisticAmmoProviderComponent, PriceCalculationEvent>(OnBallisticPrice);
}
private void OnBallisticPrice(EntityUid uid, BallisticAmmoProviderComponent component, ref PriceCalculationEvent args)
{
if (string.IsNullOrEmpty(component.FillProto) || component.UnspawnedCount == 0)
return;
if (!ProtoManager.TryIndex<EntityPrototype>(component.FillProto, out var proto))
{
Sawmill.Error($"Unable to find fill prototype for price on {component.FillProto} on {ToPrettyString(uid)}");
return;
}
// Probably good enough for most.
var price = _pricing.GetEstimatedPrice(proto);
args.Price += price * component.UnspawnedCount;
}
public override void Shoot(GunComponent gun, List<IShootable> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null)
{
// Try a clumsy roll
if (TryComp<ClumsyComponent>(user, out var clumsy))
{
for (var i = 0; i < ammo.Count; i++)
{
if (_interaction.TryRollClumsy(user.Value, GunClumsyChance, clumsy))
{
// Wound them
Damageable.TryChangeDamage(user, clumsy.ClumsyDamage);
_stun.TryParalyze(user.Value, TimeSpan.FromSeconds(3f), true);
// Apply salt to the wound ("Honk!")
Audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"), gun.Owner);
Audio.PlayPvs(new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"), gun.Owner);
PopupSystem.PopupEntity(Loc.GetString("gun-clumsy"), user.Value, Filter.Pvs(user.Value, entityManager: EntityManager));
Del(gun.Owner);
return;
}
}
}
var fromMap = fromCoordinates.ToMap(EntityManager);
var toMap = toCoordinates.ToMapPos(EntityManager);
var mapDirection = toMap - fromMap.Position;
var mapAngle = mapDirection.ToAngle();
var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle());
// Update shot based on the recoil
toMap = fromMap.Position + angle.ToVec() * mapDirection.Length;
mapDirection = toMap - fromMap.Position;
var entityDirection = Transform(fromCoordinates.EntityId).InvWorldMatrix.Transform(toMap) - fromCoordinates.Position;
// I must be high because this was getting tripped even when true.
// DebugTools.Assert(direction != Vector2.Zero);
var shotProjectiles = new List<EntityUid>(ammo.Count);
foreach (var shootable in ammo)
{
switch (shootable)
{
// Cartridge shoots something else
case CartridgeAmmoComponent cartridge:
if (!cartridge.Spent)
{
if (cartridge.Count > 1)
{
var angles = LinearSpread(mapAngle - cartridge.Spread / 2,
mapAngle + cartridge.Spread / 2, cartridge.Count);
for (var i = 0; i < cartridge.Count; i++)
{
var uid = Spawn(cartridge.Prototype, fromMap);
ShootProjectile(uid, angles[i].ToVec(), user, gun.ProjectileSpeed);
shotProjectiles.Add(uid);
}
}
else
{
var uid = Spawn(cartridge.Prototype, fromMap);
ShootProjectile(uid, mapDirection, user, gun.ProjectileSpeed);
shotProjectiles.Add(uid);
}
//signal ChemicalAmmo to transfer solution
RaiseLocalEvent(cartridge.Owner, new AmmoShotEvent()
{
FiredProjectiles = shotProjectiles,
}, false);
SetCartridgeSpent(cartridge, true);
MuzzleFlash(gun.Owner, cartridge, user);
PlaySound(gun.Owner, gun.SoundGunshot?.GetSound(Random, ProtoManager), user);
if (cartridge.DeleteOnSpawn)
Del(cartridge.Owner);
}
else
{
PlaySound(gun.Owner, gun.SoundEmpty?.GetSound(Random, ProtoManager), user);
}
// Something like ballistic might want to leave it in the container still
if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(cartridge.Owner))
EjectCartridge(cartridge.Owner);
Dirty(cartridge);
break;
// Ammo shoots itself
case AmmoComponent newAmmo:
shotProjectiles.Add(newAmmo.Owner);
MuzzleFlash(gun.Owner, newAmmo, user);
PlaySound(gun.Owner, gun.SoundGunshot?.GetSound(Random, ProtoManager), user);
// Do a throw
if (!HasComp<ProjectileComponent>(newAmmo.Owner))
{
RemComp<AmmoComponent>(newAmmo.Owner);
// TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
ThrowingSystem.TryThrow(newAmmo.Owner, mapDirection, gun.ProjectileSpeed, user);
break;
}
ShootProjectile(newAmmo.Owner, mapDirection, user, gun.ProjectileSpeed);
break;
case HitscanPrototype hitscan:
var ray = new CollisionRay(fromMap.Position, mapDirection.Normalized, hitscan.CollisionMask);
var rayCastResults =
Physics.IntersectRay(fromMap.MapId, ray, hitscan.MaxLength, user, false).ToList();
if (rayCastResults.Count >= 1)
{
var result = rayCastResults[0];
var hitEntity = result.HitEntity;
var distance = result.Distance;
FireEffects(fromCoordinates, distance, mapDirection.ToAngle(), hitscan, hitEntity);
if (hitscan.StaminaDamage > 0f)
_stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage);
var dmg = hitscan.Damage;
if (dmg != null)
dmg = Damageable.TryChangeDamage(hitEntity, dmg);
if (dmg != null)
{
if (dmg.Total > FixedPoint2.Zero)
{
RaiseNetworkEvent(new DamageEffectEvent(Color.Red, new List<EntityUid> {result.HitEntity}), Filter.Pvs(hitEntity, entityManager: EntityManager));
}
PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound);
if (user != null)
{
Logs.Add(LogType.HitScanHit,
$"{ToPrettyString(user.Value):user} hit {ToPrettyString(hitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
}
else
{
Logs.Add(LogType.HitScanHit,
$"Hit {ToPrettyString(hitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
}
}
}
else
{
FireEffects(fromCoordinates, hitscan.MaxLength, mapDirection.ToAngle(), hitscan);
}
PlaySound(gun.Owner, gun.SoundGunshot?.GetSound(Random, ProtoManager), user);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
RaiseLocalEvent(gun.Owner, new AmmoShotEvent()
{
FiredProjectiles = shotProjectiles,
}, false);
}
public void ShootProjectile(EntityUid uid, Vector2 direction, EntityUid? user = null, float speed = 20f)
{
var physics = EnsureComp<PhysicsComponent>(uid);
physics.BodyStatus = BodyStatus.InAir;
physics.LinearVelocity = direction.Normalized * speed;
if (user != null)
{
var projectile = EnsureComp<ProjectileComponent>(uid);
Projectiles.SetShooter(projectile, user.Value);
}
Transform(uid).WorldRotation = direction.ToWorldAngle();
}
/// <summary>
/// Gets a linear spread of angles between start and end.
/// </summary>
/// <param name="start">Start angle in degrees</param>
/// <param name="end">End angle in degrees</param>
/// <param name="intervals">How many shots there are</param>
private Angle[] LinearSpread(Angle start, Angle end, int intervals)
{
var angles = new Angle[intervals];
DebugTools.Assert(intervals > 1);
for (var i = 0; i <= intervals - 1; i++)
{
angles[i] = new Angle(start + (end - start) * i / (intervals - 1));
}
return angles;
}
private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction)
{
var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds;
var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease.Theta - component.AngleDecay.Theta * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
component.CurrentAngle = new Angle(newTheta);
component.LastFire = component.NextFire;
// Convert it so angle can go either side.
var random = Random.NextFloat(-0.5f, 0.5f);
var spread = component.CurrentAngle.Theta * random;
var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random);
DebugTools.Assert(spread <= component.MaxAngle.Theta);
return angle;
}
protected override void PlaySound(EntityUid gun, string? sound, EntityUid? user = null)
{
if (string.IsNullOrEmpty(sound)) return;
SoundSystem.Play(sound, Filter.Pvs(gun, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user), gun);
}
protected override void Popup(string message, EntityUid? uid, EntityUid? user) {}
protected override void CreateEffect(EntityUid uid, MuzzleFlashEvent message, EntityUid? user = null)
{
var filter = Filter.Pvs(uid, entityManager: EntityManager);
if (TryComp<ActorComponent>(user, out var actor))
filter.RemovePlayer(actor.PlayerSession);
RaiseNetworkEvent(message, filter);
}
public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
{
if (Deleted(otherEntity))
return;
// Like projectiles and melee,
// 1. Entity specific sound
// 2. Ammo's sound
// 3. Nothing
var playedSound = false;
// woops the other entity is deleted
// someone needs to handle this better. for now i'm just gonna make it not crash the server -rane
if (Deleted(otherEntity))
return;
if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.Total > 0 && TryComp<RangedDamageSoundComponent>(otherEntity, out var rangedSound))
{
var type = MeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, ProtoManager);
if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
{
Audio.PlayPvs(damageSoundType, otherEntity, AudioParams.Default.WithVariation(DamagePitchVariation));
playedSound = true;
}
else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
{
Audio.PlayPvs(damageSoundGroup, otherEntity, AudioParams.Default.WithVariation(DamagePitchVariation));
playedSound = true;
}
}
if (!playedSound && weaponSound != null)
{
Audio.PlayPvs(weaponSound, otherEntity);
}
}
// TODO: Pseudo RNG so the client can predict these.
#region Hitscan effects
private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle mapDirection, HitscanPrototype hitscan, EntityUid? hitEntity = null)
{
// Lord
// Forgive me for the shitcode I am about to do
// Effects tempt me not
var sprites = new List<(EntityCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
var gridUid = fromCoordinates.GetGridUid(EntityManager);
var angle = mapDirection;
// We'll get the effects relative to the grid / map of the firer
// Look you could probably optimise this a bit with redundant transforms at this point.
if (TryComp<TransformComponent>(gridUid, out var gridXform))
{
var (_, gridRot, gridInvMatrix) = gridXform.GetWorldPositionRotationInvMatrix();
fromCoordinates = new EntityCoordinates(gridUid.Value,
gridInvMatrix.Transform(fromCoordinates.ToMapPos(EntityManager)));
// Use the fallback angle I guess?
angle -= gridRot;
}
if (distance >= 1f)
{
if (hitscan.MuzzleFlash != null)
{
sprites.Add((fromCoordinates.Offset(angle.ToVec().Normalized / 2), angle, hitscan.MuzzleFlash, 1f));
}
if (hitscan.TravelFlash != null)
{
sprites.Add((fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2), angle, hitscan.TravelFlash, distance - 1.5f));
}
}
if (hitscan.ImpactFlash != null)
{
sprites.Add((fromCoordinates.Offset(angle.ToVec() * distance), angle.FlipPositive(), hitscan.ImpactFlash, 1f));
}
if (sprites.Count > 0)
{
RaiseNetworkEvent(new HitscanEvent
{
Sprites = sprites,
}, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
}
}
#endregion
}

View File

@@ -0,0 +1,73 @@
using Content.Server.Weapons.Ranged.Components;
using Content.Shared.Examine;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed class RechargeBasicEntityAmmoSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedGunSystem _gun = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RechargeBasicEntityAmmoComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<RechargeBasicEntityAmmoComponent, ExaminedEvent>(OnExamined);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var (recharge, ammo) in
EntityQuery<RechargeBasicEntityAmmoComponent, BasicEntityAmmoProviderComponent>())
{
if (ammo.Count is null || ammo.Count == ammo.Capacity)
continue;
recharge.AccumulatedFrameTime += frameTime;
if (recharge.AccumulatedFrameTime < recharge.NextRechargeTime)
continue;
recharge.AccumulatedFrameTime -= recharge.NextRechargeTime;
UpdateCooldown(recharge);
if (_gun.UpdateBasicEntityAmmoCount(ammo.Owner, ammo.Count.Value + 1, ammo))
{
SoundSystem.Play(recharge.RechargeSound.GetSound(), Filter.Pvs(recharge.Owner), recharge.Owner,
recharge.RechargeSound.Params);
}
}
}
private void OnInit(EntityUid uid, RechargeBasicEntityAmmoComponent component, ComponentInit args)
{
UpdateCooldown(component);
}
private void OnExamined(EntityUid uid, RechargeBasicEntityAmmoComponent component, ExaminedEvent args)
{
if (!TryComp<BasicEntityAmmoProviderComponent>(uid, out var ammo)
|| ammo.Count == ammo.Capacity)
{
args.PushMarkup(Loc.GetString("recharge-basic-entity-ammo-full"));
return;
}
var timeLeft = component.NextRechargeTime - component.AccumulatedFrameTime;
args.PushMarkup(Loc.GetString("recharge-basic-entity-ammo-can-recharge", ("seconds", Math.Round(timeLeft, 1))));
}
private void UpdateCooldown(RechargeBasicEntityAmmoComponent component)
{
component.NextRechargeTime = _random.NextFloat(component.MinRechargeCooldown, component.MaxRechargeCooldown);
}
}

View File

@@ -0,0 +1,195 @@
using Content.Server.Ghost.Components;
using Content.Shared.Administration;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics.Joints;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed class TetherGunSystem : SharedTetherGunSystem
{
[Dependency] private readonly IConGroupController _admin = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedJointSystem _joints = default!;
private readonly Dictionary<ICommonSession, (EntityUid Entity, EntityUid Tether, Joint Joint)> _tethered = new();
private readonly HashSet<ICommonSession> _draggers = new();
private const string JointId = "tether-joint";
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<StartTetherEvent>(OnStartTether);
SubscribeNetworkEvent<StopTetherEvent>(OnStopTether);
SubscribeNetworkEvent<TetherMoveEvent>(OnMoveTether);
_playerManager.PlayerStatusChanged += OnStatusChange;
}
private void OnStatusChange(object? sender, SessionStatusEventArgs e)
{
StopTether(e.Session);
}
public override void Shutdown()
{
base.Shutdown();
_playerManager.PlayerStatusChanged -= OnStatusChange;
}
public void Toggle(ICommonSession? session)
{
if (session == null)
return;
if (_draggers.Add(session))
{
RaiseNetworkEvent(new TetherGunToggleMessage()
{
Enabled = true,
}, session.ConnectedClient);
return;
}
_draggers.Remove(session);
RaiseNetworkEvent(new TetherGunToggleMessage()
{
Enabled = false,
}, session.ConnectedClient);
}
public bool IsEnabled(ICommonSession? session)
{
if (session == null)
return false;
return _draggers.Contains(session);
}
private void OnStartTether(StartTetherEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession is not IPlayerSession playerSession ||
!_admin.CanCommand(playerSession, CommandName) ||
!Exists(msg.Entity) ||
Deleted(msg.Entity) ||
msg.Coordinates == MapCoordinates.Nullspace ||
_tethered.ContainsKey(args.SenderSession)) return;
var tether = Spawn("TetherEntity", msg.Coordinates);
if (!TryComp<PhysicsComponent>(tether, out var bodyA) ||
!TryComp<PhysicsComponent>(msg.Entity, out var bodyB))
{
Del(tether);
return;
}
EnsureComp<AdminFrozenComponent>(msg.Entity);
if (TryComp<TransformComponent>(msg.Entity, out var xform))
{
xform.Anchored = false;
}
if (_container.IsEntityInContainer(msg.Entity))
{
xform?.AttachToGridOrMap();
}
if (TryComp<PhysicsComponent>(msg.Entity, out var body))
{
body.BodyStatus = BodyStatus.InAir;
}
var joint = _joints.CreateMouseJoint(bodyA.Owner, bodyB.Owner, id: JointId);
SharedJointSystem.LinearStiffness(5f, 0.7f, bodyA.Mass, bodyB.Mass, out var stiffness, out var damping);
joint.Stiffness = stiffness;
joint.Damping = damping;
joint.MaxForce = 10000f * bodyB.Mass;
_tethered.Add(playerSession, (msg.Entity, tether, joint));
RaiseNetworkEvent(new PredictTetherEvent()
{
Entity = msg.Entity
}, args.SenderSession.ConnectedClient);
}
private void OnStopTether(StopTetherEvent msg, EntitySessionEventArgs args)
{
StopTether(args.SenderSession);
}
private void StopTether(ICommonSession session)
{
if (!_tethered.TryGetValue(session, out var weh))
return;
RemComp<AdminFrozenComponent>(weh.Entity);
if (TryComp<PhysicsComponent>(weh.Entity, out var body) &&
!HasComp<GhostComponent>(weh.Entity))
{
Timer.Spawn(1000, () =>
{
if (Deleted(body.Owner)) return;
body.BodyStatus = BodyStatus.OnGround;
});
}
_joints.RemoveJoint(weh.Joint);
Del(weh.Tether);
_tethered.Remove(session);
}
private void OnMoveTether(TetherMoveEvent msg, EntitySessionEventArgs args)
{
if (!_tethered.TryGetValue(args.SenderSession, out var tether) ||
!TryComp<TransformComponent>(tether.Tether, out var xform) ||
xform.MapID != msg.Coordinates.MapId) return;
xform.WorldPosition = msg.Coordinates.Position;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var toRemove = new RemQueue<ICommonSession>();
var bodyQuery = GetEntityQuery<PhysicsComponent>();
foreach (var (session, entity) in _tethered)
{
if (Deleted(entity.Entity) ||
Deleted(entity.Tether) ||
!entity.Joint.Enabled)
{
toRemove.Add(session);
continue;
}
// Force it awake, always
if (bodyQuery.TryGetComponent(entity.Entity, out var body))
{
body.WakeBody();
}
}
foreach (var session in toRemove)
{
StopTether(session);
}
}
}