Files
tbd-station-14/Content.Shared/Damage/Systems/DamageableSystem.cs
metalgearsloth 2f07270744 Add examines for damage values (#11090)
* Add examines for damage values

Even immersive sims still give you values.

We should also do this for armour so people don't have to yml dive and so the general public actually know the balance of things.

* Slightly better

* Cleanup
2022-09-08 18:08:14 -05:00

376 lines
14 KiB
C#

using System.Linq;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Content.Shared.Radiation.Events;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Damage
{
public sealed class DamageableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
SubscribeLocalEvent<DamageableComponent, ComponentHandleState>(DamageableHandleState);
SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
SubscribeLocalEvent<DamageableComponent, OnIrradiatedEvent>(OnIrradiated);
}
/// <summary>
/// Retrieves the damage examine values.
/// </summary>
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)
{
msg.PushNewline();
msg.AddMarkup(Loc.GetString("damage-value", ("type", damage.Key), ("amount", damage.Value)));
}
return msg;
}
/// <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.DamagePerGroup = component.Damage.GetDamagePerGroup(_prototypeManager);
component.TotalDamage = component.Damage.Total;
}
/// <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(DamageableComponent damageable, DamageSpecifier damage)
{
damageable.Damage = damage;
DamageChanged(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(DamageableComponent component, DamageSpecifier? damageDelta = null,
bool interruptsDoAfters = true)
{
component.DamagePerGroup = component.Damage.GetDamagePerGroup(_prototypeManager);
component.TotalDamage = component.Damage.Total;
Dirty(component);
if (EntityManager.TryGetComponent<AppearanceComponent>(component.Owner, out var appearance) && damageDelta != null)
{
var data = new DamageVisualizerGroupData(damageDelta.GetDamagePerGroup(_prototypeManager).Keys.ToList());
appearance.SetData(DamageVisualizerKeys.DamageUpdateGroups, data);
}
RaiseLocalEvent(component.Owner, new DamageChangedEvent(component, damageDelta, interruptsDoAfters), false);
}
/// <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)
{
if (!uid.HasValue || !Resolve(uid.Value, ref damageable, false))
{
// TODO BODY SYSTEM pass damage onto body system
return null;
}
if (damage == null)
{
Logger.Error("Null DamageSpecifier. Probably because a required yaml field was not given.");
return null;
}
if (damage.Empty)
{
return damage;
}
// Apply resistances
if (!ignoreResistances)
{
if (damageable.DamageModifierSetId != null &&
_prototypeManager.TryIndex<DamageModifierSetPrototype>(damageable.DamageModifierSetId, out var modifierSet))
{
damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
}
var ev = new DamageModifyEvent(damage);
RaiseLocalEvent(uid.Value, ev, false);
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(damageable, delta, interruptsDoAfters);
}
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(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(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)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, 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 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(component, delta);
}
}
/// <summary>
/// Takes the damage from one entity and scales it relative to the health of another
/// </summary>
/// <param name="ent1">The entity whose damage will be scaled</param>
/// <param name="ent2">The entity whose health the damage will scale to</param>
/// <param name="damage">The newly scaled damage. Can be null</param>
public bool GetScaledDamage(EntityUid ent1, EntityUid ent2, out DamageSpecifier? damage)
{
damage = null;
if (!TryComp<DamageableComponent>(ent1, out var olddamage))
return false;
if (!TryComp<MobStateComponent>(ent1, out var oldstate) ||
!TryComp<MobStateComponent>(ent2, out var newstate))
return false;
int ent1DeadState = 0;
foreach (var state in oldstate._highestToLowestStates)
{
if (state.Value == DamageState.Dead)
{
ent1DeadState = state.Key;
}
}
int ent2DeadState = 0;
foreach (var state in newstate._highestToLowestStates)
{
if (state.Value == DamageState.Dead)
{
ent2DeadState = state.Key;
}
}
damage = (olddamage.Damage / ent1DeadState) * ent2DeadState;
return true;
}
}
/// <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 DamageSpecifier Damage;
public DamageModifyEvent(DamageSpecifier damage)
{
Damage = damage;
}
}
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 = false;
/// <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 = false;
public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters)
{
Damageable = damageable;
DamageDelta = damageDelta;
if (DamageDelta == null)
return;
foreach (var damageChange in DamageDelta.DamageDict.Values)
{
if (damageChange > 0)
{
DamageIncreased = true;
break;
}
}
InterruptsDoAfters = interruptsDoAfters && DamageIncreased;
}
}
}