Files
wwdpublic/Content.Shared/Damage/Systems/DamageableSystem.cs
Solaris a64b5164e3 Port AI Sentry Turrets (#1990)
# 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)
2025-03-21 18:28:40 +03:00

470 lines
19 KiB
C#

using System.Linq;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Radiation.Events;
using Content.Shared.Rejuvenate;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
// Shitmed Change
using Content.Shared.Body.Systems;
using Content.Shared._Shitmed.Targeting;
using Robust.Shared.Random;
namespace Content.Shared.Damage
{
public sealed class DamageableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly SharedBodySystem _body = default!; // Shitmed Change
[Dependency] private readonly IRobustRandom _random = default!; // Shitmed Change
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
private EntityQuery<AppearanceComponent> _appearanceQuery;
private EntityQuery<DamageableComponent> _damageableQuery;
private EntityQuery<MindContainerComponent> _mindContainerQuery;
public override void Initialize()
{
SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
SubscribeLocalEvent<DamageableComponent, ComponentHandleState>(DamageableHandleState);
SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
SubscribeLocalEvent<DamageableComponent, OnIrradiatedEvent>(OnIrradiated);
SubscribeLocalEvent<DamageableComponent, RejuvenateEvent>(OnRejuvenate);
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_damageableQuery = GetEntityQuery<DamageableComponent>();
_mindContainerQuery = GetEntityQuery<MindContainerComponent>();
}
/// <summary>
/// Initialize a damageable component
/// </summary>
private void DamageableInit(EntityUid uid, DamageableComponent component, ComponentInit _)
{
if (component.DamageContainerID != null &&
_prototypeManager.TryIndex<DamageContainerPrototype>(component.DamageContainerID,
out var damageContainerPrototype))
{
// Initialize damage dictionary, using the types and groups from the damage
// container prototype
foreach (var type in damageContainerPrototype.SupportedTypes)
{
component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
}
foreach (var groupId in damageContainerPrototype.SupportedGroups)
{
var group = _prototypeManager.Index<DamageGroupPrototype>(groupId);
foreach (var type in group.DamageTypes)
{
component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
}
}
}
else
{
// No DamageContainerPrototype was given. So we will allow the container to support all damage types
foreach (var type in _prototypeManager.EnumeratePrototypes<DamageTypePrototype>())
{
component.Damage.DamageDict.TryAdd(type.ID, FixedPoint2.Zero);
}
}
component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
component.TotalDamage = component.Damage.GetTotal();
}
/// <summary>
/// Directly sets the damage specifier of a damageable component.
/// </summary>
/// <remarks>
/// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed
/// event is raised.
/// </remarks>
public void SetDamage(EntityUid uid, DamageableComponent damageable, DamageSpecifier damage)
{
damageable.Damage = damage;
DamageChanged(uid, damageable);
}
/// <summary>
/// If the damage in a DamageableComponent was changed, this function should be called.
/// </summary>
/// <remarks>
/// This updates cached damage information, flags the component as dirty, and raises a damage changed event.
/// The damage changed event is used by other systems, such as damage thresholds.
/// </remarks>
public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null,
bool interruptsDoAfters = true, EntityUid? origin = null, bool? canSever = null) // Shitmed Change
{
component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
component.TotalDamage = component.Damage.GetTotal();
Dirty(uid, component);
if (_appearanceQuery.TryGetComponent(uid, out var appearance) && damageDelta != null)
{
var data = new DamageVisualizerGroupData(component.DamagePerGroup.Keys.ToList());
_appearance.SetData(uid, DamageVisualizerKeys.DamageUpdateGroups, data, appearance);
}
RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin, canSever ?? true)); // Shitmed Change
}
/// <summary>
/// Applies damage specified via a <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// <see cref="DamageSpecifier"/> is effectively just a dictionary of damage types and damage values. This
/// function just applies the container's resistances (unless otherwise specified) and then changes the
/// stored damage data. Division of group damage into types is managed by <see cref="DamageSpecifier"/>.
/// </remarks>
/// <returns>
/// Returns a <see cref="DamageSpecifier"/> with information about the actual damage changes. This will be
/// null if the user had no applicable components that can take damage.
/// </returns>
public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false,
bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null,
// Shitmed Change
bool? canSever = true, bool? canEvade = false, float? partMultiplier = 1.00f, TargetBodyPart? targetPart = null, bool doPartDamage = true)
{
if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false))
{
// TODO BODY SYSTEM pass damage onto body system
return null;
}
if (damage.Empty)
{
return damage;
}
var before = new BeforeDamageChangedEvent(damage, origin, targetPart, canEvade ?? false); // Shitmed Change
RaiseLocalEvent(uid.Value, ref before);
if (before.Cancelled)
return null;
// Shitmed Change Start
if (doPartDamage)
{
var partDamage = new TryChangePartDamageEvent(damage, origin, targetPart, ignoreResistances, canSever ?? true, canEvade ?? false, partMultiplier ?? 1.00f);
RaiseLocalEvent(uid.Value, ref partDamage);
if (partDamage.Evaded || partDamage.Cancelled)
return null;
}
// Shitmed Change End
// Apply resistances
if (!ignoreResistances)
{
if (damageable.DamageModifierSetId != null &&
_prototypeManager.TryIndex<DamageModifierSetPrototype>(damageable.DamageModifierSetId, out var modifierSet))
{
// TODO DAMAGE PERFORMANCE
// use a local private field instead of creating a new dictionary here..
// TODO: We need to add a check to see if the given armor covers the targeted part (if any) to modify or not.
damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
}
// From Solidus: If you are reading this, I owe you a more comprehensive refactor of this entire system.
if (damageable.DamageModifierSets.Count > 0)
foreach (var enumerableModifierSet in damageable.DamageModifierSets)
if (_prototypeManager.TryIndex<DamageModifierSetPrototype>(enumerableModifierSet, out var enumerableModifier))
damage = DamageSpecifier.ApplyModifierSet(damage, enumerableModifier);
var ev = new DamageModifyEvent(damage, origin, targetPart); // Shitmed Change
RaiseLocalEvent(uid.Value, ev);
damage = ev.Damage;
if (damage.Empty)
{
return damage;
}
}
// TODO DAMAGE PERFORMANCE
// Consider using a local private field instead of creating a new dictionary here.
// Would need to check that nothing ever tries to cache the delta.
var delta = new DamageSpecifier();
delta.DamageDict.EnsureCapacity(damage.DamageDict.Count);
var dict = damageable.Damage.DamageDict;
foreach (var (type, value) in damage.DamageDict)
{
// CollectionsMarshal my beloved.
if (!dict.TryGetValue(type, out var oldValue))
continue;
var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value);
if (newValue == oldValue)
continue;
dict[type] = newValue;
delta.DamageDict[type] = newValue - oldValue;
}
if (delta.DamageDict.Count > 0)
DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin, canSever); // Shitmed Change
return delta;
}
/// <summary>
/// Sets all damage types supported by a <see cref="DamageableComponent"/> to the specified value.
/// </summary>
/// <remakrs>
/// Does nothing If the given damage value is negative.
/// </remakrs>
public void SetAllDamage(EntityUid uid, DamageableComponent component, FixedPoint2 newValue)
{
if (newValue < 0)
{
// invalid value
return;
}
foreach (var type in component.Damage.DamageDict.Keys)
{
component.Damage.DamageDict[type] = newValue;
}
// Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an
// empty damage delta.
DamageChanged(uid, component, new DamageSpecifier());
// Shitmed Change Start
if (HasComp<TargetingComponent>(uid))
{
foreach (var (part, _) in _body.GetBodyChildren(uid))
{
if (!TryComp(part, out DamageableComponent? damageComp))
continue;
SetAllDamage(part, damageComp, newValue);
}
}
// Shitmed Change End
}
/// <summary>
/// Changes all damage types supported by a <see cref="DamageableComponent"/> by the specified value.
/// </summary>
/// <remakrs>
/// Will not lower damage to a negative value.
/// </remakrs>
public void ChangeAllDamage(EntityUid uid, DamageableComponent component, FixedPoint2 addedValue)
{
foreach (var type in component.Damage.DamageDict.Keys)
{
component.Damage.DamageDict[type] += addedValue;
if (component.Damage.DamageDict[type] < 0)
component.Damage.DamageDict[type] = 0;
}
// Changing damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an
// empty damage delta.
DamageChanged(uid, component, new DamageSpecifier());
// Shitmed Change Start
if (!HasComp<TargetingComponent>(uid))
return;
foreach (var (part, _) in _body.GetBodyChildren(uid))
{
if (!TryComp(part, out DamageableComponent? damageComp))
continue;
ChangeAllDamage(part, damageComp, addedValue);
}
// Shitmed Change End
}
public void SetDamageModifierSetId(EntityUid uid, string? damageModifierSetId, DamageableComponent? comp = null)
{
if (!_damageableQuery.Resolve(uid, ref comp))
return;
comp.DamageModifierSetId = damageModifierSetId;
Dirty(uid, comp);
}
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
{
if (_netMan.IsServer)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId);
}
else
{
// avoid mispredicting damage on newly spawned entities.
args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageModifierSetId);
}
}
private void OnIrradiated(EntityUid uid, DamageableComponent component, OnIrradiatedEvent args)
{
var damageValue = FixedPoint2.New(args.TotalRads);
// Radiation should really just be a damage group instead of a list of types.
DamageSpecifier damage = new();
foreach (var typeId in component.RadiationDamageTypeIDs)
{
damage.DamageDict.Add(typeId, damageValue);
}
TryChangeDamage(uid, damage, interruptsDoAfters: false);
}
private void OnRejuvenate(EntityUid uid, DamageableComponent component, RejuvenateEvent args)
{
TryComp<MobThresholdsComponent>(uid, out var thresholds);
_mobThreshold.SetAllowRevives(uid, true, thresholds); // do this so that the state changes when we set the damage
SetAllDamage(uid, component, 0);
_mobThreshold.SetAllowRevives(uid, false, thresholds);
}
private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args)
{
if (args.Current is not DamageableComponentState state)
{
return;
}
component.DamageModifierSetId = state.ModifierSetId;
// Has the damage actually changed?
DamageSpecifier newDamage = new() { DamageDict = new(state.DamageDict) };
var delta = component.Damage - newDamage;
delta.TrimZeros();
if (!delta.Empty)
{
component.Damage = newDamage;
DamageChanged(uid, component, delta);
}
}
}
/// <summary>
/// Raised before damage is done, so stuff can cancel it if necessary.
/// </summary>
[ByRefEvent]
public record struct BeforeDamageChangedEvent(
DamageSpecifier Damage,
EntityUid? Origin = null,
TargetBodyPart? TargetPart = null, // Shitmed Change
bool CanEvade = false, // Lavaland Change
bool Cancelled = false);
/// <summary>
/// Shitmed Change: Raised on parts before damage is done so we can cancel the damage if they evade.
/// </summary>
[ByRefEvent]
public record struct TryChangePartDamageEvent(
DamageSpecifier Damage,
EntityUid? Origin = null,
TargetBodyPart? TargetPart = null,
bool IgnoreResistances = false,
bool CanSever = true,
bool CanEvade = false,
float PartMultiplier = 1.00f,
bool Evaded = false,
bool Cancelled = false);
/// <summary>
/// Raised on an entity when damage is about to be dealt,
/// in case anything else needs to modify it other than the base
/// damageable component.
///
/// For example, armor.
/// </summary>
public sealed class DamageModifyEvent : EntityEventArgs, IInventoryRelayEvent
{
// Whenever locational damage is a thing, this should just check only that bit of armour.
public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET;
public readonly DamageSpecifier OriginalDamage;
public DamageSpecifier Damage;
public EntityUid? Origin;
public readonly TargetBodyPart? TargetPart; // Shitmed Change
public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null, TargetBodyPart? targetPart = null) // Shitmed Change
{
OriginalDamage = damage;
Damage = damage;
Origin = origin;
TargetPart = targetPart; // Shitmed Change
}
}
public sealed class DamageChangedEvent : EntityEventArgs
{
/// <summary>
/// This is the component whose damage was changed.
/// </summary>
/// <remarks>
/// Given that nearly every component that cares about a change in the damage, needs to know the
/// current damage values, directly passing this information prevents a lot of duplicate
/// Owner.TryGetComponent() calls.
/// </remarks>
public readonly DamageableComponent Damageable;
/// <summary>
/// The amount by which the damage has changed. If the damage was set directly to some number, this will be
/// null.
/// </summary>
public readonly DamageSpecifier? DamageDelta;
/// <summary>
/// Was any of the damage change dealing damage, or was it all healing?
/// </summary>
public readonly bool DamageIncreased;
/// <summary>
/// Does this event interrupt DoAfters?
/// Note: As provided in the constructor, this *does not* account for DamageIncreased.
/// As written into the event, this *does* account for DamageIncreased.
/// </summary>
public readonly bool InterruptsDoAfters;
/// <summary>
/// Contains the entity which caused the change in damage, if any was responsible.
/// </summary>
public readonly EntityUid? Origin;
/// <summary>
/// Shitmed Change: Can this damage event sever parts?
/// </summary>
public readonly bool CanSever;
public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin, bool canSever = true) // Shitmed Change
{
Damageable = damageable;
DamageDelta = damageDelta;
Origin = origin;
CanSever = canSever; // Shitmed Change
if (DamageDelta == null)
return;
foreach (var damageChange in DamageDelta.DamageDict.Values)
{
if (damageChange > 0)
{
DamageIncreased = true;
break;
}
}
InterruptsDoAfters = interruptsDoAfters && DamageIncreased;
}
}
}