using System.Linq; using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; using Content.Shared.Inventory; 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; 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 MobThresholdSystem _mobThreshold = default!; public override void Initialize() { SubscribeLocalEvent(DamageableInit); SubscribeLocalEvent(DamageableHandleState); SubscribeLocalEvent(DamageableGetState); SubscribeLocalEvent(OnIrradiated); SubscribeLocalEvent(OnRejuvenate); } /// /// Retrieves the damage examine values. /// public FormattedMessage GetDamageExamine(DamageSpecifier damageSpecifier, string? type = null) { var msg = new FormattedMessage(); if (string.IsNullOrEmpty(type)) { msg.AddMarkup(Loc.GetString("damage-examine")); } else { msg.AddMarkup(Loc.GetString("damage-examine-type", ("type", type))); } foreach (var damage in damageSpecifier.DamageDict) { if (damage.Value != FixedPoint2.Zero) { msg.PushNewline(); msg.AddMarkup(Loc.GetString("damage-value", ("type", damage.Key), ("amount", damage.Value))); } } return msg; } /// /// Initialize a damageable component /// private void DamageableInit(EntityUid uid, DamageableComponent component, ComponentInit _) { if (component.DamageContainerID != null && _prototypeManager.TryIndex(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(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()) { component.Damage.DamageDict.TryAdd(type.ID, FixedPoint2.Zero); } } component.DamagePerGroup = component.Damage.GetDamagePerGroup(_prototypeManager); component.TotalDamage = component.Damage.Total; } /// /// Directly sets the damage specifier of a damageable component. /// /// /// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed /// event is raised. /// public void SetDamage(EntityUid uid, DamageableComponent damageable, DamageSpecifier damage) { damageable.Damage = damage; DamageChanged(uid, damageable); } /// /// If the damage in a DamageableComponent was changed, this function should be called. /// /// /// 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. /// public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null, bool interruptsDoAfters = true, EntityUid? origin = null) { component.DamagePerGroup = component.Damage.GetDamagePerGroup(_prototypeManager); component.TotalDamage = component.Damage.Total; Dirty(component); if (EntityManager.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)); } /// /// Applies damage specified via a . /// /// /// 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 . /// /// /// Returns a with information about the actual damage changes. This will be /// null if the user had no applicable components that can take damage. /// public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false, bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null) { if (!uid.HasValue || !Resolve(uid.Value, ref damageable, false)) { // TODO BODY SYSTEM pass damage onto body system return null; } if (damage == null) { Log.Error("Null DamageSpecifier. Probably because a required yaml field was not given."); return null; } if (damage.Empty) { return damage; } var before = new BeforeDamageChangedEvent(damage); RaiseLocalEvent(uid.Value, ref before); if (before.Cancelled) return null; // Apply resistances if (!ignoreResistances) { if (damageable.DamageModifierSetId != null && _prototypeManager.TryIndex(damageable.DamageModifierSetId, out var modifierSet)) { damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); } var ev = new DamageModifyEvent(damage); RaiseLocalEvent(uid.Value, ev); damage = ev.Damage; if (damage.Empty) { return damage; } } // Copy the current damage, for calculating the difference DamageSpecifier oldDamage = new(damageable.Damage); damageable.Damage.ExclusiveAdd(damage); damageable.Damage.ClampMin(FixedPoint2.Zero); var delta = damageable.Damage - oldDamage; delta.TrimZeros(); if (!delta.Empty) { DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin); } return delta; } /// /// Sets all damage types supported by a to the specified value. /// /// /// Does nothing If the given damage value is negative. /// 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()); } public void SetDamageModifierSetId(EntityUid uid, string damageModifierSetId, DamageableComponent? comp = null) { if (!Resolve(uid, ref comp)) return; comp.DamageModifierSetId = damageModifierSetId; Dirty(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); } private void OnRejuvenate(EntityUid uid, DamageableComponent component, RejuvenateEvent args) { TryComp(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); } } } /// /// Raised before damage is done, so stuff can cancel it if necessary. /// [ByRefEvent] public record struct BeforeDamageChangedEvent(DamageSpecifier Delta, bool Cancelled=false); /// /// 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. /// 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 DamageModifyEvent(DamageSpecifier damage) { OriginalDamage = damage; Damage = damage; } } public sealed class DamageChangedEvent : EntityEventArgs { /// /// This is the component whose damage was changed. /// /// /// 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. /// public readonly DamageableComponent Damageable; /// /// The amount by which the damage has changed. If the damage was set directly to some number, this will be /// null. /// public readonly DamageSpecifier? DamageDelta; /// /// Was any of the damage change dealing damage, or was it all healing? /// public readonly bool DamageIncreased = false; /// /// 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. /// public readonly bool InterruptsDoAfters = false; /// /// Contains the entity which caused the change in damage, if any was responsible. /// public readonly EntityUid? Origin; public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin) { Damageable = damageable; DamageDelta = damageDelta; Origin = origin; if (DamageDelta == null) return; foreach (var damageChange in DamageDelta.DamageDict.Values) { if (damageChange > 0) { DamageIncreased = true; break; } } InterruptsDoAfters = interruptsDoAfters && DamageIncreased; } } }