mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-20 23:17:43 +03:00
# Description I am trying to port over the AI turrets being implemented into wizden made by chromiumboy. It looks fantastic and would like to port this now and work on any issues that might show. --- # Original PRs https://github.com/space-wizards/space-station-14/issues/35223 https://github.com/space-wizards/space-station-14/pull/35025 https://github.com/space-wizards/space-station-14/pull/35031 https://github.com/space-wizards/space-station-14/pull/35058 https://github.com/space-wizards/space-station-14/pull/35123 https://github.com/space-wizards/space-station-14/pull/35149 https://github.com/space-wizards/space-station-14/pull/35235 https://github.com/space-wizards/space-station-14/pull/35236 --- # TODO - [x] Port all related PRs to EE. - [x] Patch any bugs with turrets or potential issues. - [x] Cleanup my shitcode or changes. --- # Changelog 🆑 - add: Added recharging sentry turrets, one is AI-based or the other is Sec can make. - add: The sentry turrets can be made after researching in T3 arsenal. The boards are made in the sec fab. - add: New ID permissions for borgs and minibots for higher turret options. - tweak: Turrets stop shooting after someone goes crit. --------- Co-authored-by: Nathaniel Adams <60526456+Nathaniel-Adams@users.noreply.github.com> (cherry picked from commit 209d0537401cbda448a03e910cca9a898c9d566f)
211 lines
7.2 KiB
C#
211 lines
7.2 KiB
C#
using Content.Server.NPC.Components;
|
|
using Content.Shared.CombatMode;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Physics;
|
|
using Content.Shared.Weapons.Ranged.Components;
|
|
using Content.Shared.Weapons.Ranged.Events;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Physics.Components;
|
|
|
|
namespace Content.Server.NPC.Systems;
|
|
|
|
public sealed partial class NPCCombatSystem
|
|
{
|
|
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
|
[Dependency] private readonly RotateToFaceSystem _rotate = default!;
|
|
[Dependency] private readonly MapSystem _map = default!;
|
|
|
|
private EntityQuery<CombatModeComponent> _combatQuery;
|
|
private EntityQuery<NPCSteeringComponent> _steeringQuery;
|
|
private EntityQuery<RechargeBasicEntityAmmoComponent> _rechargeQuery;
|
|
private EntityQuery<PhysicsComponent> _physicsQuery;
|
|
private EntityQuery<TransformComponent> _xformQuery;
|
|
|
|
// TODO: Don't predict for hitscan
|
|
private const float ShootSpeed = 20f;
|
|
|
|
/// <summary>
|
|
/// Cooldown on raycasting to check LOS.
|
|
/// </summary>
|
|
public const float UnoccludedCooldown = 0.2f;
|
|
|
|
private void InitializeRanged()
|
|
{
|
|
_combatQuery = GetEntityQuery<CombatModeComponent>();
|
|
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
|
_rechargeQuery = GetEntityQuery<RechargeBasicEntityAmmoComponent>();
|
|
_steeringQuery = GetEntityQuery<NPCSteeringComponent>();
|
|
_xformQuery = GetEntityQuery<TransformComponent>();
|
|
|
|
SubscribeLocalEvent<NPCRangedCombatComponent, ComponentStartup>(OnRangedStartup);
|
|
SubscribeLocalEvent<NPCRangedCombatComponent, ComponentShutdown>(OnRangedShutdown);
|
|
}
|
|
|
|
private void OnRangedStartup(EntityUid uid, NPCRangedCombatComponent component, ComponentStartup args)
|
|
{
|
|
if (TryComp<CombatModeComponent>(uid, out var combat))
|
|
{
|
|
_combat.SetInCombatMode(uid, true, combat);
|
|
}
|
|
else
|
|
{
|
|
component.Status = CombatStatus.Unspecified;
|
|
}
|
|
}
|
|
|
|
private void OnRangedShutdown(EntityUid uid, NPCRangedCombatComponent component, ComponentShutdown args)
|
|
{
|
|
if (TryComp<CombatModeComponent>(uid, out var combat))
|
|
{
|
|
_combat.SetInCombatMode(uid, false, combat);
|
|
}
|
|
}
|
|
|
|
private void UpdateRanged(float frameTime)
|
|
{
|
|
var query = EntityQueryEnumerator<NPCRangedCombatComponent, TransformComponent>();
|
|
|
|
while (query.MoveNext(out var uid, out var comp, out var xform))
|
|
{
|
|
if (comp.Status == CombatStatus.Unspecified)
|
|
continue;
|
|
|
|
if (_steeringQuery.TryGetComponent(uid, out var steering) && steering.Status == SteeringStatus.NoPath)
|
|
{
|
|
comp.Status = CombatStatus.TargetUnreachable;
|
|
comp.ShootAccumulator = 0f;
|
|
continue;
|
|
}
|
|
|
|
if (!_xformQuery.TryGetComponent(comp.Target, out var targetXform) ||
|
|
!_physicsQuery.TryGetComponent(comp.Target, out var targetBody))
|
|
{
|
|
comp.Status = CombatStatus.TargetUnreachable;
|
|
comp.ShootAccumulator = 0f;
|
|
continue;
|
|
}
|
|
|
|
if (targetXform.MapID != xform.MapID)
|
|
{
|
|
comp.Status = CombatStatus.TargetUnreachable;
|
|
comp.ShootAccumulator = 0f;
|
|
continue;
|
|
}
|
|
|
|
if (_combatQuery.TryGetComponent(uid, out var combatMode))
|
|
{
|
|
_combat.SetInCombatMode(uid, true, combatMode);
|
|
}
|
|
|
|
if (!_gun.TryGetGun(uid, out var gunUid, out var gun))
|
|
{
|
|
comp.Status = CombatStatus.NoWeapon;
|
|
comp.ShootAccumulator = 0f;
|
|
continue;
|
|
}
|
|
|
|
var ammoEv = new GetAmmoCountEvent();
|
|
RaiseLocalEvent(gunUid, ref ammoEv);
|
|
|
|
if (ammoEv.Count == 0)
|
|
{
|
|
// Recharging then?
|
|
if (_rechargeQuery.HasComponent(gunUid))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
comp.Status = CombatStatus.Unspecified;
|
|
comp.ShootAccumulator = 0f;
|
|
continue;
|
|
}
|
|
|
|
comp.LOSAccumulator -= frameTime;
|
|
|
|
var worldPos = _transform.GetWorldPosition(xform);
|
|
var targetPos = _transform.GetWorldPosition(targetXform);
|
|
|
|
// We'll work out the projected spot of the target and shoot there instead of where they are.
|
|
var distance = (targetPos - worldPos).Length();
|
|
var oldInLos = comp.TargetInLOS;
|
|
|
|
// TODO: Should be doing these raycasts in parallel
|
|
// Ideally we'd have 2 steps, 1. to go over the normal details for shooting and then 2. to handle beep / rotate / shoot
|
|
if (comp.LOSAccumulator < 0f)
|
|
{
|
|
comp.LOSAccumulator += UnoccludedCooldown;
|
|
|
|
// For consistency with NPC steering.
|
|
var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
|
|
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
|
|
}
|
|
|
|
if (!comp.TargetInLOS)
|
|
{
|
|
comp.ShootAccumulator = 0f;
|
|
comp.Status = CombatStatus.NotInSight;
|
|
|
|
if (TryComp(uid, out steering))
|
|
{
|
|
steering.ForceMove = true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!oldInLos && comp.SoundTargetInLOS != null)
|
|
{
|
|
_audio.PlayPvs(comp.SoundTargetInLOS, uid);
|
|
}
|
|
|
|
comp.ShootAccumulator += frameTime;
|
|
|
|
if (comp.ShootAccumulator < comp.ShootDelay)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var mapVelocity = targetBody.LinearVelocity;
|
|
var targetSpot = targetPos + mapVelocity * distance / ShootSpeed;
|
|
|
|
// If we have a max rotation speed then do that.
|
|
var goalRotation = (targetSpot - worldPos).ToWorldAngle();
|
|
var rotationSpeed = comp.RotationSpeed;
|
|
|
|
if (!_rotate.TryRotateTo(uid, goalRotation, frameTime, comp.AccuracyThreshold, rotationSpeed?.Theta ?? double.MaxValue, xform))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// TODO: LOS
|
|
// TODO: Ammo checks
|
|
// TODO: Burst fire
|
|
// TODO: Cycling
|
|
// Max rotation speed
|
|
|
|
// TODO: Check if we can face
|
|
|
|
if (!Enabled || !_gun.CanShoot(gun))
|
|
continue;
|
|
|
|
EntityCoordinates targetCordinates;
|
|
|
|
if (_mapManager.TryFindGridAt(xform.MapID, targetPos, out var gridUid, out var mapGrid))
|
|
targetCordinates = new EntityCoordinates(gridUid, mapGrid.WorldToLocal(targetSpot));
|
|
else
|
|
targetCordinates = new EntityCoordinates(xform.MapUid!.Value, targetSpot);
|
|
|
|
comp.Status = CombatStatus.Normal;
|
|
|
|
if (gun.NextFire > _timing.CurTime)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_gun.SetTarget(gun, comp.Target); // WWDP set target to hit prone enemies
|
|
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates);
|
|
}
|
|
}
|
|
}
|