ECS damageable (#4529)
* ECS and damage Data * Comments and newlines * Added Comments * Make TryChangeDamageEvent immutable * Remove SetAllDamage event Use public SetAllDamage function instead * Undo destructible mistakes That was some shit code. * Rename DamageData to DamageSpecifier And misc small edits misc * Cache trigger prototypes. * Renaming destructible classes & functions * Revert "Cache trigger prototypes." This reverts commit 86bae15ba6616884dba75f552dfdfbe2d1fb6586. * Replace prototypes with prototype IDs. * Split damage.yml into individual files * move get/handle component state to system * Update HealthChange doc * Make godmode call Dirty() on damageable component * Add Initialize() to fix damage test * Make non-static * uncache resistance set prototype and trim DamageableComponentState * Remove unnecessary Dirty() calls during initialization * RemoveTryChangeDamageEvent * revert Dirty() * Fix MobState relying on DamageableComponent.Dirty() * Fix DisposalUnit Tests. These were previously failing, but because the async was not await-ed, this never raised the exception. After I fixed MobState component, this exception stopped happening and instead the assertions started being tested & failing * Disposal test 2: electric boogaloo * Fix typos/mistakes also add comments and fix spacing. * Use Uids instead of IEntity * fix merge * Comments, a merge issue, and making some damage ignore resistances * Extend DamageSpecifier and use it for DamageableComponent * fix master merge * Fix Disposal unit test. Again. Snapgrids were removed in master * Execute Exectute
This commit is contained in:
@@ -1,499 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Acts;
|
||||
using Content.Shared.Damage.Container;
|
||||
using Content.Shared.Damage.Resistances;
|
||||
using Content.Shared.Radiation;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Damage.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that allows attached entities to take damage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The supported damage types are specified using a <see cref="DamageContainerPrototype"/>s. DamageContainers
|
||||
/// are effectively a dictionary of damage types and damage numbers, along with functions to modify them. Damage
|
||||
/// groups are collections of damage types. A damage group is 'applicable' to a damageable component if it
|
||||
/// supports at least one damage type in that group. A subset of these groups may be 'fully supported' when every
|
||||
/// member of the group is supported by the container. This basic version never dies (thus can take an
|
||||
/// indefinite amount of damage).
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IDamageableComponent))]
|
||||
[NetworkedComponent()]
|
||||
public class DamageableComponent : Component, IDamageableComponent, IRadiationAct, ISerializationHooks
|
||||
{
|
||||
public override string Name => "Damageable";
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The main damage dictionary. All the damage information is stored in this dictionary with <see cref="DamageTypePrototype"/> keys.
|
||||
/// </summary>
|
||||
private Dictionary<DamageTypePrototype, int> _damageDict = new();
|
||||
|
||||
[DataField("resistances")]
|
||||
public string ResistanceSetId { get; set; } = "defaultResistances";
|
||||
|
||||
[ViewVariables] public ResistanceSet Resistances { get; set; } = new();
|
||||
|
||||
// TODO DAMAGE Use as default values, specify overrides in a separate property through yaml for better (de)serialization
|
||||
[ViewVariables]
|
||||
[DataField("damageContainer")]
|
||||
public string DamageContainerId { get; set; } = "metallicDamageContainer";
|
||||
|
||||
// TODO DAMAGE Cache this
|
||||
// When moving logic from damageableComponent --> Damage System, make damageSystem update these on damage change.
|
||||
[ViewVariables] public int TotalDamage => _damageDict.Values.Sum();
|
||||
[ViewVariables] public IReadOnlyDictionary<DamageTypePrototype, int> GetDamagePerType => _damageDict;
|
||||
[ViewVariables] public IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerApplicableGroup => DamageTypeDictToDamageGroupDict(_damageDict, ApplicableDamageGroups);
|
||||
[ViewVariables] public IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerFullySupportedGroup => DamageTypeDictToDamageGroupDict(_damageDict, FullySupportedDamageGroups);
|
||||
|
||||
// Whenever sending over network, also need a <string, int> dictionary
|
||||
// TODO DAMAGE MAYBE Cache this?
|
||||
public IReadOnlyDictionary<string, int> GetDamagePerApplicableGroupIDs => ConvertDictKeysToIDs(GetDamagePerApplicableGroup);
|
||||
public IReadOnlyDictionary<string, int> GetDamagePerFullySupportedGroupIDs => ConvertDictKeysToIDs(GetDamagePerFullySupportedGroup);
|
||||
public IReadOnlyDictionary<string, int> GetDamagePerTypeIDs => ConvertDictKeysToIDs(_damageDict);
|
||||
|
||||
// TODO PROTOTYPE Replace these datafield variables with prototype references, once they are supported.
|
||||
// Also requires appropriate changes in OnExplosion() and RadiationAct()
|
||||
[ViewVariables]
|
||||
[DataField("radiationDamageTypes")]
|
||||
public List<string> RadiationDamageTypeIDs { get; set; } = new() {"Radiation"};
|
||||
[ViewVariables]
|
||||
[DataField("explosionDamageTypes")]
|
||||
public List<string> ExplosionDamageTypeIDs { get; set; } = new() { "Piercing", "Heat" };
|
||||
|
||||
public HashSet<DamageGroupPrototype> ApplicableDamageGroups { get; } = new();
|
||||
|
||||
public HashSet<DamageGroupPrototype> FullySupportedDamageGroups { get; } = new();
|
||||
|
||||
public HashSet<DamageTypePrototype> SupportedDamageTypes { get; } = new();
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// TODO DAMAGE Serialize damage done and resistance changes
|
||||
var damageContainerPrototype = _prototypeManager.Index<DamageContainerPrototype>(DamageContainerId);
|
||||
|
||||
ApplicableDamageGroups.Clear();
|
||||
FullySupportedDamageGroups.Clear();
|
||||
SupportedDamageTypes.Clear();
|
||||
|
||||
//Get Damage groups/types from the DamageContainerPrototype.
|
||||
DamageContainerId = damageContainerPrototype.ID;
|
||||
ApplicableDamageGroups.UnionWith(damageContainerPrototype.ApplicableDamageGroups);
|
||||
FullySupportedDamageGroups.UnionWith(damageContainerPrototype.FullySupportedDamageGroups);
|
||||
SupportedDamageTypes.UnionWith(damageContainerPrototype.SupportedDamageTypes);
|
||||
|
||||
//initialize damage dictionary 0 damage
|
||||
_damageDict = new(SupportedDamageTypes.Count);
|
||||
foreach (var type in SupportedDamageTypes)
|
||||
{
|
||||
_damageDict.Add(type, 0);
|
||||
}
|
||||
|
||||
Resistances = new ResistanceSet(_prototypeManager.Index<ResistanceSetPrototype>(ResistanceSetId));
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
ForceHealthChangedEvent();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
return new DamageableComponentState(GetDamagePerTypeIDs);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (!(curState is DamageableComponentState state))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_damageDict.Clear();
|
||||
|
||||
foreach (var (type, damage) in state.DamageDict)
|
||||
{
|
||||
_damageDict[_prototypeManager.Index<DamageTypePrototype>(type)] = damage;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetDamage(DamageTypePrototype type)
|
||||
{
|
||||
return GetDamagePerType.GetValueOrDefault(type);
|
||||
}
|
||||
|
||||
public bool TryGetDamage(DamageTypePrototype type, out int damage)
|
||||
{
|
||||
return GetDamagePerType.TryGetValue(type, out damage);
|
||||
}
|
||||
|
||||
public int GetDamage(DamageGroupPrototype group)
|
||||
{
|
||||
return GetDamagePerApplicableGroup.GetValueOrDefault(group);
|
||||
}
|
||||
|
||||
public bool TryGetDamage(DamageGroupPrototype group, out int damage)
|
||||
{
|
||||
return GetDamagePerApplicableGroup.TryGetValue(group, out damage);
|
||||
}
|
||||
|
||||
public bool IsApplicableDamageGroup(DamageGroupPrototype group)
|
||||
{
|
||||
return ApplicableDamageGroups.Contains(group);
|
||||
}
|
||||
|
||||
public bool IsFullySupportedDamageGroup(DamageGroupPrototype group)
|
||||
{
|
||||
return FullySupportedDamageGroups.Contains(group);
|
||||
}
|
||||
|
||||
public bool IsSupportedDamageType(DamageTypePrototype type)
|
||||
{
|
||||
return SupportedDamageTypes.Contains(type);
|
||||
}
|
||||
|
||||
public bool TrySetDamage(DamageGroupPrototype group, int newValue)
|
||||
{
|
||||
if (!ApplicableDamageGroups.Contains(group))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newValue < 0)
|
||||
{
|
||||
// invalid value
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
TrySetDamage(type, newValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TrySetAllDamage(int newValue)
|
||||
{
|
||||
if (newValue < 0)
|
||||
{
|
||||
// invalid value
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var type in SupportedDamageTypes)
|
||||
{
|
||||
TrySetDamage(type, newValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false)
|
||||
{
|
||||
// Check if damage type is supported, and get the current value if it is.
|
||||
if (!GetDamagePerType.TryGetValue(type, out var current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply resistances (does nothing if amount<0)
|
||||
var finalDamage = amount;
|
||||
if (!ignoreDamageResistances)
|
||||
{
|
||||
finalDamage = Resistances.CalculateDamage(type, amount);
|
||||
}
|
||||
|
||||
if (finalDamage == 0)
|
||||
return false;
|
||||
|
||||
// Are we healing below zero?
|
||||
if (current + finalDamage < 0)
|
||||
{
|
||||
if (current == 0)
|
||||
// Damage type is supported, but there is nothing to do
|
||||
return false;
|
||||
|
||||
// Cap healing down to zero
|
||||
_damageDict[type] = 0;
|
||||
finalDamage = -current;
|
||||
}
|
||||
else
|
||||
{
|
||||
_damageDict[type] = current + finalDamage;
|
||||
}
|
||||
|
||||
current = _damageDict[type];
|
||||
|
||||
var datum = new DamageChangeData(type, current, finalDamage);
|
||||
var data = new List<DamageChangeData> {datum};
|
||||
|
||||
OnHealthChanged(data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false)
|
||||
{
|
||||
var types = group.DamageTypes.ToArray();
|
||||
|
||||
if (amount < 0)
|
||||
{
|
||||
// We are Healing. Keep track of how much we can hand out (with a better var name for readability).
|
||||
var availableHealing = -amount;
|
||||
|
||||
// Get total group damage.
|
||||
var damageToHeal = GetDamagePerApplicableGroup[group];
|
||||
|
||||
// Is there any damage to even heal?
|
||||
if (damageToHeal == 0)
|
||||
return false;
|
||||
|
||||
// If total healing is more than there is damage, just set to 0 and return.
|
||||
if (damageToHeal <= availableHealing)
|
||||
{
|
||||
TrySetDamage(group, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Partially heal each damage group
|
||||
int healing, damage;
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (!_damageDict.TryGetValue(type, out damage))
|
||||
{
|
||||
// Damage Type is not supported. Continue without reducing availableHealing
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply healing to the damage type. The healing amount may be zero if either damage==0, or if
|
||||
// integer rounding made it zero (i.e., damage is small)
|
||||
healing = (availableHealing * damage) / damageToHeal;
|
||||
TryChangeDamage(type, -healing, ignoreDamageResistances);
|
||||
|
||||
// remove this damage type from the damage we consider for future loops, regardless of how much we
|
||||
// actually healed this type.
|
||||
damageToHeal -= damage;
|
||||
availableHealing -= healing;
|
||||
|
||||
// If we now healed all the damage, exit. otherwise 1/0 and universe explodes.
|
||||
if (damageToHeal == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Damage type is supported, there was damage to heal, and resistances were ignored
|
||||
// --> Damage must have changed
|
||||
return true;
|
||||
}
|
||||
else if (amount > 0)
|
||||
{
|
||||
// Resistances may result in no actual damage change. We need to keep track if any damage got through.
|
||||
var damageChanged = false;
|
||||
|
||||
// We are adding damage. Keep track of how much we can dish out (with a better var name for readability).
|
||||
var availableDamage = amount;
|
||||
|
||||
// How many damage types do we have to distribute over?.
|
||||
var numberDamageTypes = types.Length;
|
||||
|
||||
// Apply damage to each damage group
|
||||
int damage;
|
||||
foreach (var type in types)
|
||||
{
|
||||
// Distribute the remaining damage over the remaining damage types.
|
||||
damage = availableDamage / numberDamageTypes;
|
||||
|
||||
// Try apply the damage type. If damage type is not supported, this has no effect.
|
||||
// We also use the return value to check whether any damage has changed
|
||||
damageChanged = TryChangeDamage(type, damage, ignoreDamageResistances) || damageChanged;
|
||||
|
||||
// regardless of whether we dealt damage, reduce the amount to distribute.
|
||||
availableDamage -= damage;
|
||||
numberDamageTypes -= 1;
|
||||
|
||||
}
|
||||
return damageChanged;
|
||||
}
|
||||
|
||||
// amount==0 no damage change.
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TrySetDamage(DamageTypePrototype type, int newValue)
|
||||
{
|
||||
if (!_damageDict.TryGetValue(type, out var oldValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newValue < 0)
|
||||
{
|
||||
// invalid value
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oldValue == newValue)
|
||||
{
|
||||
// No health change.
|
||||
// But we are trying to set, not trying to change.
|
||||
return true;
|
||||
}
|
||||
|
||||
_damageDict[type] = newValue;
|
||||
|
||||
var delta = newValue - oldValue;
|
||||
var datum = new DamageChangeData(type, 0, delta);
|
||||
var data = new List<DamageChangeData> {datum};
|
||||
|
||||
OnHealthChanged(data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ForceHealthChangedEvent()
|
||||
{
|
||||
var data = new List<DamageChangeData>();
|
||||
|
||||
foreach (var type in SupportedDamageTypes)
|
||||
{
|
||||
var damage = GetDamage(type);
|
||||
var datum = new DamageChangeData(type, damage, 0);
|
||||
data.Add(datum);
|
||||
}
|
||||
|
||||
OnHealthChanged(data);
|
||||
}
|
||||
|
||||
private void OnHealthChanged(List<DamageChangeData> changes)
|
||||
{
|
||||
var args = new DamageChangedEventArgs(this, changes);
|
||||
OnHealthChanged(args);
|
||||
}
|
||||
|
||||
protected virtual void OnHealthChanged(DamageChangedEventArgs e)
|
||||
{
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, e);
|
||||
|
||||
var message = new DamageChangedMessage(this, e.Data);
|
||||
SendMessage(message);
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public void RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
|
||||
{
|
||||
var totalDamage = Math.Max((int)(frameTime * radiation.RadsPerSecond), 1);
|
||||
|
||||
foreach (var typeID in RadiationDamageTypeIDs)
|
||||
{
|
||||
TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(typeID), totalDamage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void OnExplosion(ExplosionEventArgs eventArgs)
|
||||
{
|
||||
var damage = eventArgs.Severity switch
|
||||
{
|
||||
ExplosionSeverity.Light => 20,
|
||||
ExplosionSeverity.Heavy => 60,
|
||||
ExplosionSeverity.Destruction => 250,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
foreach (var typeID in ExplosionDamageTypeIDs)
|
||||
{
|
||||
TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(typeID), damage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Take a dictionary with <see cref="IPrototype"/> keys and return a dictionary using <see cref="IPrototype.ID"/> as keys
|
||||
/// instead.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Useful when sending damage type and group prototypes dictionaries over the network.
|
||||
/// </remarks>
|
||||
public static IReadOnlyDictionary<string, int>
|
||||
ConvertDictKeysToIDs<TPrototype>(IReadOnlyDictionary<TPrototype, int> prototypeDict)
|
||||
where TPrototype : IPrototype
|
||||
{
|
||||
Dictionary<string, int> idDict = new(prototypeDict.Count);
|
||||
foreach (var entry in prototypeDict)
|
||||
{
|
||||
idDict.Add(entry.Key.ID, entry.Value);
|
||||
}
|
||||
return idDict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a dictionary with damage type keys to a dictionary of damage groups keys.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Takes a dictionary with damage types as keys and integers as values, and an iterable list of damage
|
||||
/// groups. Returns a dictionary with damage group keys, with values calculated by adding up the values for
|
||||
/// each damage type in that group. If a damage type is associated with more than one supported damage
|
||||
/// group, it will contribute to the total of each group. Conversely, some damage types may not contribute
|
||||
/// to the new dictionary if their associated group(s) are not in given list of groups.
|
||||
/// </remarks>
|
||||
public static IReadOnlyDictionary<DamageGroupPrototype, int>
|
||||
DamageTypeDictToDamageGroupDict(IReadOnlyDictionary<DamageTypePrototype, int> damageTypeDict, IEnumerable<DamageGroupPrototype> groupKeys)
|
||||
{
|
||||
var damageGroupDict = new Dictionary<DamageGroupPrototype, int>();
|
||||
int damageGroupSumDamage, damageTypeDamage;
|
||||
// iterate over the list of group keys for our new dictionary
|
||||
foreach (var group in groupKeys)
|
||||
{
|
||||
// For each damage type in this group, add up the damage present in the given dictionary
|
||||
damageGroupSumDamage = 0;
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
// if the damage type is in the dictionary, add it's damage to the group total.
|
||||
if (damageTypeDict.TryGetValue(type, out damageTypeDamage))
|
||||
{
|
||||
damageGroupSumDamage += damageTypeDamage;
|
||||
}
|
||||
}
|
||||
damageGroupDict.Add(group, damageGroupSumDamage);
|
||||
}
|
||||
return damageGroupDict;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class DamageableComponentState : ComponentState
|
||||
{
|
||||
public readonly IReadOnlyDictionary<string, int> DamageDict;
|
||||
|
||||
public DamageableComponentState(IReadOnlyDictionary<string, int> damageDict)
|
||||
|
||||
{
|
||||
DamageDict = damageDict;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Acts;
|
||||
using Content.Shared.Damage.Resistances;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Damage.Components
|
||||
{
|
||||
public interface IDamageableComponent : IComponent, IExAct
|
||||
{
|
||||
/// <summary>
|
||||
/// The sum of all damages types in the DamageableComponent.
|
||||
/// </summary>
|
||||
int TotalDamage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of the damage in the container, indexed by applicable <see cref="DamageGroupPrototype"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values represent the sum of all damage in each group. If a supported damage type is a member of more than one group, it will contribute to each one.
|
||||
/// Therefore, the sum of the values may be greater than the sum of the values in the dictionary returned by <see cref="GetDamagePerType"/>
|
||||
/// </remarks>
|
||||
IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerApplicableGroup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of the damage in the container, indexed by fully supported instances of <see
|
||||
/// cref="DamageGroupPrototype"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values represent the sum of all damage in each group. As the damage container may have some damage
|
||||
/// types that are not part of a fully supported damage group, the sum of the values may be less of the values
|
||||
/// in the dictionary returned by <see cref="GetDamagePerType"/>. On the other hand, if a supported damage type
|
||||
/// is a member of more than one group, it will contribute to each one. Therefore, the sum may also be greater
|
||||
/// instead.
|
||||
/// </remarks>
|
||||
IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerFullySupportedGroup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of the damage in the container, indexed by <see cref="DamageTypePrototype"/>.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<DamageTypePrototype, int> GetDamagePerType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="GetDamagePerApplicableGroup"/>, but indexed by <see cref="DamageGroupPrototype.ID"/>
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, int> GetDamagePerApplicableGroupIDs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="GetDamagePerFullySupportedGroup"/>, but indexed by <see cref="DamageGroupPrototype.ID"/>
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, int> GetDamagePerFullySupportedGroupIDs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="GetDamagePerType"/>, but indexed by <see cref="DamageTypePrototype.ID"/>
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, int> GetDamagePerTypeIDs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of damage types supported by this DamageableComponent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each of these damage types is fully supported. If any of these damage types is a
|
||||
/// member of a damage group, these groups are represented in <see cref="ApplicableDamageGroups"></see>
|
||||
/// </remarks>
|
||||
HashSet<DamageTypePrototype> SupportedDamageTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of damage groups that are fully supported by DamageableComponent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This describes what damage groups this damage container explicitly supports. It supports every damage type
|
||||
/// contained in these damage groups. It may also support other damage types not in these groups. To see all
|
||||
/// damage types <see cref="SupportedDamageTypes"/>, and to see all applicable damage groups <see
|
||||
/// cref="ApplicableDamageGroups"/>.
|
||||
/// </remarks>
|
||||
HashSet<DamageGroupPrototype> FullySupportedDamageGroups { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of damage groups that could apply damage to this DamageableComponent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This describes what damage groups could have an effect on this damage container. However not every damage
|
||||
/// group has to be fully supported. For example, the container may support ONLY the piercing damage type. It should
|
||||
/// therefore be affected by instances of brute damage, but does not necessarily support blunt or slash damage.
|
||||
/// For a list of supported damage types, see <see cref="SupportedDamageTypes"/>.
|
||||
/// </remarks>
|
||||
HashSet<DamageGroupPrototype> ApplicableDamageGroups { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The resistances of this component.
|
||||
/// </summary>
|
||||
ResistanceSet Resistances { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the amount of damage of a type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to get the damage of.</param>
|
||||
/// <param name="damage">The amount of damage of that type.</param>
|
||||
/// <returns>
|
||||
/// True if the given <see cref="type"/> is supported, false otherwise.
|
||||
/// </returns>
|
||||
bool TryGetDamage(DamageTypePrototype type, out int damage);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the amount of damage of a given type, or zero if it is not supported.
|
||||
/// </summary>
|
||||
int GetDamage(DamageTypePrototype type);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the total amount of damage in a damage group.
|
||||
/// </summary>
|
||||
/// <param name="group">The group to get the damage of.</param>
|
||||
/// <param name="damage">The amount of damage in that group.</param>
|
||||
/// <returns>
|
||||
/// True if the given group is applicable to this container, false otherwise.
|
||||
/// </returns>
|
||||
bool TryGetDamage(DamageGroupPrototype group, out int damage);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the amount of damage present in an applicable group, or zero if no members are supported.
|
||||
/// </summary>
|
||||
int GetDamage(DamageGroupPrototype group);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to change the specified <see cref="DamageTypePrototype"/>, applying
|
||||
/// resistance values only if it is dealing damage.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of damage being changed.</param>
|
||||
/// <param name="amount">
|
||||
/// Amount of damage being received (positive for damage, negative for heals).
|
||||
/// </param>
|
||||
/// <param name="ignoreDamageResistances">
|
||||
/// Whether or not to ignore resistances when taking damage.
|
||||
/// Healing always ignores resistances, regardless of this input.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// False if the given type is not supported or no damage change occurred; true otherwise.
|
||||
/// </returns>
|
||||
bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to change damage of the specified <see cref="DamageGroupPrototype"/>, applying resistance values
|
||||
/// only if it is damage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If dealing damage, this spreads the damage change amount evenly between the <see
|
||||
/// cref="DamageTypePrototype"></see>s in this group (subject to integer rounding). If only a subset of the
|
||||
/// damage types in the group are actually supported, then the total damage dealt may be less than expected
|
||||
/// (unsupported damage is ignored).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If healing damage, this spreads the damage change proportional to the current damage value of each <see
|
||||
/// cref="DamageTypePrototype"></see> (subject to integer rounding). If there is less damage than is being
|
||||
/// healed, some healing is wasted. Unsupported damage types do not waste healing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="group">group of damage being changed.</param>
|
||||
/// <param name="amount">
|
||||
/// Amount of damage being received (positive for damage, negative for heals).
|
||||
/// </param>
|
||||
/// <param name="ignoreDamageResistances">
|
||||
/// Whether to ignore resistances when taking damage. Healing always ignores resistances, regardless of this
|
||||
/// input.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Returns false if the given group is not applicable or no damage change occurred; true otherwise.
|
||||
/// </returns>
|
||||
bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false);
|
||||
|
||||
/// <summary>
|
||||
/// Forcefully sets the specified <see cref="DamageTypePrototype"/> to the given value, ignoring resistance
|
||||
/// values.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of damage being set.</param>
|
||||
/// <param name="newValue">New damage value to be set.</param>
|
||||
/// <returns>
|
||||
/// Returns false if a given type is not supported or a negative value is provided; true otherwise.
|
||||
/// </returns>
|
||||
bool TrySetDamage(DamageTypePrototype type, int newValue);
|
||||
|
||||
/// <summary>
|
||||
/// Forcefully sets all damage types in a specified damage group using <see cref="TrySetDamage"></see>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that the actual damage of this group will be equal to the given value times the number damage group
|
||||
/// members that this container supports.
|
||||
/// </remarks>
|
||||
/// <param name="group">Group of damage being set.</param>
|
||||
/// <param name="newValue">New damage value to be set.</param>
|
||||
/// <returns>
|
||||
/// Returns false if the given group is not applicable or a negative value is provided; true otherwise.
|
||||
/// </returns>
|
||||
bool TrySetDamage(DamageGroupPrototype group, int newValue);
|
||||
|
||||
/// <summary>
|
||||
/// Sets all supported damage types to specified value using <see cref="TrySetDamage"></see>.
|
||||
/// </summary>
|
||||
/// <param name="newValue">New damage value to be set.</param>
|
||||
/// <returns>
|
||||
/// Returns false if a negative value is provided; true otherwise.
|
||||
/// </returns>
|
||||
bool TrySetAllDamage(int newValue);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given damage group is applicable to this damage container.
|
||||
/// </summary>
|
||||
public bool IsApplicableDamageGroup(DamageGroupPrototype group);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given damage group is fully supported by this damage container.
|
||||
/// </summary>
|
||||
public bool IsFullySupportedDamageGroup(DamageGroupPrototype group);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given damage type is supported by this damage container.
|
||||
/// </summary>
|
||||
public bool IsSupportedDamageType(DamageTypePrototype type);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the HealthChangedEvent with the current values of health.
|
||||
/// </summary>
|
||||
void ForceHealthChangedEvent();
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Damage.Container
|
||||
{
|
||||
/// <summary>
|
||||
/// A damage container which can be used to specify support for various damage types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is effectively just a list of damage types that can be specified in YAML files using both damage types
|
||||
/// and damage groups. Currently this is only used to specify what damage types a <see
|
||||
/// cref="Components.DamageableComponent"/> should support.
|
||||
/// </remarks>
|
||||
[Prototype("damageContainer")]
|
||||
[Serializable, NetSerializable]
|
||||
public class DamageContainerPrototype : IPrototype, ISerializationHooks
|
||||
{
|
||||
private IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this DamageContainerPrototype will support ALL damage types and groups. If true,
|
||||
/// ignore all other options.
|
||||
/// </summary>
|
||||
[DataField("supportAll")] private bool _supportAll;
|
||||
|
||||
[DataField("supportedGroups")] private HashSet<string> _supportedDamageGroupIDs = new();
|
||||
[DataField("supportedTypes")] private HashSet<string> _supportedDamageTypeIDs = new();
|
||||
|
||||
private HashSet<DamageGroupPrototype> _applicableDamageGroups = new();
|
||||
private HashSet<DamageGroupPrototype> _fullySupportedDamageGroups = new();
|
||||
private HashSet<DamageTypePrototype> _supportedDamageTypes = new();
|
||||
|
||||
// TODO NET 5 IReadOnlySet
|
||||
|
||||
/// <summary>
|
||||
/// Collection of damage groups that can affect this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This describes what damage groups can have an effect on this damage container. However not every damage
|
||||
/// group has to be fully supported. For example, the container may support ONLY the piercing damage type.
|
||||
/// It should therefore be affected by instances of brute group damage, but does not necessarily support
|
||||
/// blunt or slash damage. If damage containers are only specified by supported damage groups, and every
|
||||
/// damage type is in only one damage group, then SupportedDamageTypes should be equal to
|
||||
/// ApplicableDamageGroups. For a list of supported damage types, see <see cref="SupportedDamageTypes"/>.
|
||||
/// </remarks>
|
||||
[ViewVariables] public IReadOnlyCollection<DamageGroupPrototype> ApplicableDamageGroups => _applicableDamageGroups;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of damage groups that are fully supported by this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This describes what damage groups this damage container explicitly supports. It supports every damage
|
||||
/// type contained in these damage groups. It may also support other damage types not in these groups. To
|
||||
/// see all damage types <see cref="SupportedDamageTypes"/>, and to see all applicable damage groups <see
|
||||
/// cref="ApplicableDamageGroups"/>.
|
||||
/// </remarks>
|
||||
[ViewVariables] public IReadOnlyCollection<DamageGroupPrototype> FullySupportedDamageGroups => _fullySupportedDamageGroups;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of damage types supported by this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each of these damage types is fully supported by the DamageContainer. If any of these damage types is a
|
||||
/// member of a damage group, these groups are added to <see cref="ApplicableDamageGroups"></see>
|
||||
/// </remarks>
|
||||
[ViewVariables] public IReadOnlyCollection<DamageTypePrototype> SupportedDamageTypes => _supportedDamageTypes;
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
if (_supportAll)
|
||||
{
|
||||
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
|
||||
{
|
||||
_applicableDamageGroups.Add(group);
|
||||
_fullySupportedDamageGroups.Add(group);
|
||||
}
|
||||
foreach (var type in _prototypeManager.EnumeratePrototypes<DamageTypePrototype>())
|
||||
{
|
||||
_supportedDamageTypes.Add(type);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Add fully supported damage groups
|
||||
foreach (var groupID in _supportedDamageGroupIDs)
|
||||
{
|
||||
var group = _prototypeManager.Index<DamageGroupPrototype>(groupID);
|
||||
_fullySupportedDamageGroups.Add(group);
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
_supportedDamageTypes.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
// Add individual damage types, that are either not part of a group, or whose groups are (possibly) not fully supported
|
||||
foreach (var supportedTypeID in _supportedDamageTypeIDs)
|
||||
{
|
||||
var type = _prototypeManager.Index<DamageTypePrototype>(supportedTypeID);
|
||||
_supportedDamageTypes.Add(type);
|
||||
}
|
||||
|
||||
// For whatever reason, someone may have listed all members of a group as supported instead of just listing
|
||||
// the group as supported. Check for this.
|
||||
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
|
||||
{
|
||||
if (_fullySupportedDamageGroups.Contains(group))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// The group is not in the list of fully supported groups. Should it be?
|
||||
var allMembersSupported = true;
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
if (!_supportedDamageTypes.Contains(type))
|
||||
{
|
||||
// not all members are supported
|
||||
allMembersSupported = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allMembersSupported) {
|
||||
// All members are supported. The silly goose should have just used a damage group.
|
||||
_fullySupportedDamageGroups.Add(group);
|
||||
}
|
||||
}
|
||||
|
||||
// For each supported damage type, check whether it is in any existing group, If it is add it to _applicableDamageGroups
|
||||
foreach (var type in _supportedDamageTypes)
|
||||
{
|
||||
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
|
||||
{
|
||||
if (group.DamageTypes.Contains(type))
|
||||
{
|
||||
_applicableDamageGroups.Add(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
/// <summary>
|
||||
/// Data class with information on how the value of a
|
||||
/// single <see cref="DamageTypePrototype"/> has changed.
|
||||
/// </summary>
|
||||
public struct DamageChangeData
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of damage that changed.
|
||||
/// </summary>
|
||||
public DamageTypePrototype Type;
|
||||
|
||||
/// <summary>
|
||||
/// The new current value for that damage.
|
||||
/// </summary>
|
||||
public int NewValue;
|
||||
|
||||
/// <summary>
|
||||
/// How much the health value changed from its last value (negative is heals, positive is damage).
|
||||
/// </summary>
|
||||
public int Delta;
|
||||
|
||||
public DamageChangeData(DamageTypePrototype type, int newValue, int delta)
|
||||
{
|
||||
Type = type;
|
||||
NewValue = newValue;
|
||||
Delta = delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
public class DamageChangedEventArgs : EventArgs
|
||||
{
|
||||
public DamageChangedEventArgs(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
|
||||
{
|
||||
Damageable = damageable;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public DamageChangedEventArgs(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta)
|
||||
{
|
||||
Damageable = damageable;
|
||||
|
||||
var datum = new DamageChangeData(type, newValue, delta);
|
||||
var data = new List<DamageChangeData> {datum};
|
||||
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
|
||||
/// </summary>
|
||||
public IDamageableComponent Damageable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// List containing data on each <see cref="DamageTypePrototype"/> that was changed.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DamageChangeData> Data { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
public class DamageChangedMessage : ComponentMessage
|
||||
{
|
||||
public DamageChangedMessage(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
|
||||
{
|
||||
Damageable = damageable;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public DamageChangedMessage(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta)
|
||||
{
|
||||
Damageable = damageable;
|
||||
|
||||
var datum = new DamageChangeData(type, newValue, delta);
|
||||
var data = new List<DamageChangeData> {datum};
|
||||
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
|
||||
/// </summary>
|
||||
public IDamageableComponent Damageable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// List containing data on each <see cref="DamageTypePrototype"/> that was changed.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DamageChangeData> Data { get; }
|
||||
|
||||
public bool TookDamage
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var datum in Data)
|
||||
{
|
||||
if (datum.Delta > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
/// <summary>
|
||||
/// A Group of <see cref="DamageTypePrototype"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These groups can be used to specify supported damage types of a <see
|
||||
/// cref="Container.DamageContainerPrototype"/>, or to change/get/set damage in a <see
|
||||
/// cref="Components.DamageableComponent"/>.
|
||||
/// </remarks>
|
||||
[Prototype("damageGroup")]
|
||||
[Serializable, NetSerializable]
|
||||
public class DamageGroupPrototype : IPrototype, ISerializationHooks
|
||||
{
|
||||
private IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[DataField("id", required: true)] public string ID { get; } = default!;
|
||||
|
||||
[DataField("damageTypes", required: true)]
|
||||
public List<string> TypeIDs { get; } = default!;
|
||||
|
||||
public HashSet<DamageTypePrototype> DamageTypes { get; } = new();
|
||||
|
||||
// Create set of damage types
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
foreach (var typeID in TypeIDs)
|
||||
{
|
||||
DamageTypes.Add(_prototypeManager.Index<DamageTypePrototype>(typeID));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
375
Content.Shared/Damage/DamageSpecifier.cs
Normal file
375
Content.Shared/Damage/DamageSpecifier.cs
Normal file
@@ -0,0 +1,375 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
// TODO DAMAGE UNITS Move this whole class away from, using integers. Also get rid of a lot of the rounding. Just
|
||||
// use DamageUnit math operators.
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a collection of damage types and damage values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The actual damage information is stored in <see cref="DamageDict"/>. This class provides
|
||||
/// functions to apply resistance sets and supports basic math operations to modify this dictionary.
|
||||
/// </remarks>
|
||||
[DataDefinition]
|
||||
public class DamageSpecifier
|
||||
{
|
||||
[DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, DamageTypePrototype>))]
|
||||
private readonly Dictionary<string,int>? _damageTypeDictionary;
|
||||
|
||||
[DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, DamageGroupPrototype>))]
|
||||
private readonly Dictionary<string, int>? _damageGroupDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// Main DamageSpecifier dictionary. Most DamageSpecifier functions exist to somehow modifying this.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Dictionary<string, int> DamageDict
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_damageDict == null)
|
||||
DeserializeDamage();
|
||||
return _damageDict!;
|
||||
}
|
||||
set => _damageDict = value;
|
||||
}
|
||||
private Dictionary<string, int>? _damageDict;
|
||||
|
||||
/// <summary>
|
||||
/// Sum of the damage values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage
|
||||
/// in another. For this purpose, you should instead use <see cref="TrimZeros()"/> and then check the <see
|
||||
/// cref="Empty"/> property.
|
||||
/// </remarks>
|
||||
public int Total => DamageDict.Values.Sum();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this damage specifier has any entries.
|
||||
/// </summary>
|
||||
public bool Empty => DamageDict.Count == 0;
|
||||
|
||||
#region constructors
|
||||
/// <summary>
|
||||
/// Constructor that just results in an empty dictionary.
|
||||
/// </summary>
|
||||
public DamageSpecifier() { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor that takes another DamageSpecifier instance and copies it.
|
||||
/// </summary>
|
||||
public DamageSpecifier(DamageSpecifier damageSpec)
|
||||
{
|
||||
DamageDict = new(damageSpec.DamageDict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor that takes a single damage type prototype and a damage value.
|
||||
/// </summary>
|
||||
public DamageSpecifier(DamageTypePrototype type, int value)
|
||||
{
|
||||
DamageDict = new() { { type.ID, value } };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor that takes a single damage group prototype and a damage value. The value is divided between members of the damage group.
|
||||
/// </summary>
|
||||
public DamageSpecifier(DamageGroupPrototype group, int value)
|
||||
{
|
||||
_damageGroupDictionary = new() { { group.ID, value } };
|
||||
}
|
||||
#endregion constructors
|
||||
|
||||
/// <summary>
|
||||
/// Combines the damage group and type datafield dictionaries into a single damage dictionary.
|
||||
/// </summary>
|
||||
public void DeserializeDamage()
|
||||
{
|
||||
// Add all the damage types by just copying the type dictionary (if it is not null).
|
||||
if (_damageTypeDictionary != null)
|
||||
{
|
||||
_damageDict = new(_damageTypeDictionary);
|
||||
}
|
||||
else
|
||||
{
|
||||
_damageDict = new();
|
||||
}
|
||||
|
||||
if (_damageGroupDictionary == null)
|
||||
return;
|
||||
|
||||
// Then resolve damage groups and add them
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
foreach (var entry in _damageGroupDictionary)
|
||||
{
|
||||
if (!prototypeManager.TryIndex<DamageGroupPrototype>(entry.Key, out var group))
|
||||
{
|
||||
// This can happen if deserialized before prototypes are loaded.
|
||||
Logger.Error($"Unknown damage group given to DamageSpecifier: {entry.Key}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simply distribute evenly (except for rounding).
|
||||
// We do this by reducing remaining the # of types and damage every loop.
|
||||
var remainingTypes = group.DamageTypes.Count;
|
||||
var remainingDamage = entry.Value;
|
||||
foreach (var damageType in group.DamageTypes)
|
||||
{
|
||||
var damage = remainingDamage / remainingTypes;
|
||||
if (!_damageDict.TryAdd(damageType, damage))
|
||||
{
|
||||
// Key already exists, add values
|
||||
_damageDict[damageType] += damage;
|
||||
}
|
||||
remainingDamage -= damage;
|
||||
remainingTypes -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduce (or increase) damages by applying a resistance set.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only applies resistance to a damage type if it is dealing damage, not healing.
|
||||
/// </remarks>
|
||||
public static DamageSpecifier ApplyResistanceSet(DamageSpecifier damageSpec, ResistanceSetPrototype resistanceSet)
|
||||
{
|
||||
// Make a copy of the given data. Don't modify the one passed to this function. I did this before, and weapons became
|
||||
// duller as you hit walls. Neat, but not intended. And confusing, when you realize your fists don't work no
|
||||
// more cause they're just bloody stumps.
|
||||
DamageSpecifier newDamage = new(damageSpec);
|
||||
|
||||
foreach (var entry in newDamage.DamageDict)
|
||||
{
|
||||
if (entry.Value <= 0) continue;
|
||||
|
||||
float newValue = entry.Value;
|
||||
|
||||
if (resistanceSet.FlatReduction.TryGetValue(entry.Key, out var reduction))
|
||||
{
|
||||
newValue -= reduction;
|
||||
if (newValue <= 0)
|
||||
{
|
||||
// flat reductions cannot heal you
|
||||
newDamage.DamageDict[entry.Key] = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (resistanceSet.Coefficients.TryGetValue(entry.Key, out var coefficient))
|
||||
{
|
||||
// negative coefficients **can** heal you.
|
||||
newValue = MathF.Round(newValue*coefficient, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
newDamage.DamageDict[entry.Key] = (int) newValue;
|
||||
}
|
||||
|
||||
newDamage.TrimZeros();
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove any damage entries with zero damage.
|
||||
/// </summary>
|
||||
public void TrimZeros()
|
||||
{
|
||||
foreach (var (key, value) in DamageDict)
|
||||
{
|
||||
if (value == 0)
|
||||
{
|
||||
DamageDict.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps each damage value to be within the given range.
|
||||
/// </summary>
|
||||
public void Clamp(int minValue = 0, int maxValue = 0)
|
||||
{
|
||||
DebugTools.Assert(minValue < maxValue);
|
||||
ClampMax(maxValue);
|
||||
ClampMin(minValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all damage values to be at least as large as the given number.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this only acts on damage types present in the dictionary. It will not add new damage types.
|
||||
/// </remarks>
|
||||
public void ClampMin(int minValue = 0)
|
||||
{
|
||||
foreach (var (key, value) in DamageDict)
|
||||
{
|
||||
if (value < minValue)
|
||||
{
|
||||
DamageDict[key] = minValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all damage values to be at most some number. Note that if a damage type is not present in the
|
||||
/// dictionary, these will not be added.
|
||||
/// </summary>
|
||||
public void ClampMax(int maxValue = 0)
|
||||
{
|
||||
foreach (var (key, value) in DamageDict)
|
||||
{
|
||||
if (value > maxValue)
|
||||
{
|
||||
DamageDict[key] = maxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This adds the damage values of some other <see cref="DamageSpecifier"/> to the current one without
|
||||
/// adding any new damage types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used for <see cref="DamageableComponent"/>s, such that only "supported" damage types are
|
||||
/// actually added to the component. In most other instances, you can just use the addition operator.
|
||||
/// </remarks>
|
||||
public void ExclusiveAdd(DamageSpecifier other)
|
||||
{
|
||||
foreach (var (type, value) in other.DamageDict)
|
||||
{
|
||||
if (DamageDict.ContainsKey(type))
|
||||
{
|
||||
DamageDict[type] += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add up all the damage values for damage types that are members of a given group.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no members of the group are included in this specifier, returns false.
|
||||
/// </remarks>
|
||||
public bool TryGetDamageInGroup(DamageGroupPrototype group, out int total)
|
||||
{
|
||||
bool containsMemeber = false;
|
||||
total = 0;
|
||||
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
if (DamageDict.TryGetValue(type, out var value))
|
||||
{
|
||||
total += value;
|
||||
containsMemeber = true;
|
||||
}
|
||||
}
|
||||
return containsMemeber;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary using <see cref="DamageGroupPrototype.ID"/> keys, with values calculated by adding
|
||||
/// up the values for each damage type in that group
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a damage type is associated with more than one supported damage group, it will contribute to the
|
||||
/// total of each group. If no members of a group are present in this <see cref="DamageSpecifier"/>, the
|
||||
/// group is not included in the resulting dictionary.
|
||||
/// </remarks>
|
||||
public Dictionary<string, int> GetDamagePerGroup()
|
||||
{
|
||||
var damageGroupDict = new Dictionary<string, int>();
|
||||
foreach (var group in IoCManager.Resolve<IPrototypeManager>().EnumeratePrototypes<DamageGroupPrototype>())
|
||||
{
|
||||
if (TryGetDamageInGroup(group, out var value))
|
||||
{
|
||||
damageGroupDict.Add(group.ID, value);
|
||||
}
|
||||
}
|
||||
return damageGroupDict;
|
||||
}
|
||||
|
||||
#region Operators
|
||||
public static DamageSpecifier operator *(DamageSpecifier damageSpec, int factor)
|
||||
{
|
||||
DamageSpecifier newDamage = new();
|
||||
foreach (var entry in damageSpec.DamageDict)
|
||||
{
|
||||
newDamage.DamageDict.Add(entry.Key, entry.Value * factor);
|
||||
}
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
public static DamageSpecifier operator *(DamageSpecifier damageSpec, float factor)
|
||||
{
|
||||
DamageSpecifier newDamage = new();
|
||||
foreach (var entry in damageSpec.DamageDict)
|
||||
{
|
||||
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value * factor, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
public static DamageSpecifier operator /(DamageSpecifier damageSpec, int factor)
|
||||
{
|
||||
DamageSpecifier newDamage = new();
|
||||
foreach (var entry in damageSpec.DamageDict)
|
||||
{
|
||||
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value / (float) factor, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
public static DamageSpecifier operator /(DamageSpecifier damageSpec, float factor)
|
||||
{
|
||||
DamageSpecifier newDamage = new();
|
||||
|
||||
foreach (var entry in damageSpec.DamageDict)
|
||||
{
|
||||
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value / factor, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
public static DamageSpecifier operator +(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB)
|
||||
{
|
||||
// Copy existing dictionary from dataA
|
||||
DamageSpecifier newDamage = new(damageSpecA);
|
||||
|
||||
// Then just add types in B
|
||||
foreach (var entry in damageSpecB.DamageDict)
|
||||
{
|
||||
if (!newDamage.DamageDict.TryAdd(entry.Key, entry.Value))
|
||||
{
|
||||
// Key already exists, add values
|
||||
newDamage.DamageDict[entry.Key] += entry.Value;
|
||||
}
|
||||
}
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
public static DamageSpecifier operator -(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB) => damageSpecA + -damageSpecB;
|
||||
|
||||
public static DamageSpecifier operator +(DamageSpecifier damageSpec) => damageSpec;
|
||||
|
||||
public static DamageSpecifier operator -(DamageSpecifier damageSpec) => damageSpec * -1;
|
||||
|
||||
public static DamageSpecifier operator *(float factor, DamageSpecifier damageSpec) => damageSpec * factor;
|
||||
|
||||
public static DamageSpecifier operator *(int factor, DamageSpecifier damageSpec) => damageSpec * factor;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class DamageSystem : EntitySystem
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
129
Content.Shared/Damage/DamageableComponent.cs
Normal file
129
Content.Shared/Damage/DamageableComponent.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Acts;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Radiation;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that allows entities to take damage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The supported damage types are specified using a <see cref="DamageContainerPrototype"/>s. DamageContainers
|
||||
/// may also have resistances to certain damage types, defined via a <see cref="ResistanceSetPrototype"/>.
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent()]
|
||||
[Friend(typeof(DamageableSystem))]
|
||||
public class DamageableComponent : Component, IRadiationAct, IExAct
|
||||
{
|
||||
public override string Name => "Damageable";
|
||||
|
||||
/// <summary>
|
||||
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
|
||||
/// If null, all damage types will be supported.
|
||||
/// </summary>
|
||||
[DataField("damageContainer", customTypeSerializer: typeof(PrototypeIdSerializer<DamageContainerPrototype>))]
|
||||
public string? DamageContainerID;
|
||||
|
||||
/// <summary>
|
||||
/// This <see cref="ResistanceSetPrototype"/> will be applied to any damage that is dealt to this container,
|
||||
/// unless the damage explicitly ignores resistances.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("resistanceSet", customTypeSerializer: typeof(PrototypeIdSerializer<ResistanceSetPrototype>))]
|
||||
public string? ResistanceSetID;
|
||||
|
||||
/// <summary>
|
||||
/// All the damage information is stored in this <see cref="DamageSpecifier"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this data-field is specified, this allows damageable components to be initialized with non-zero damage.
|
||||
/// </remarks>
|
||||
[DataField("damage")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DamageSpecifier Damage = new();
|
||||
|
||||
/// <summary>
|
||||
/// Damage, indexed by <see cref="DamageGroupPrototype"/> ID keys.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Groups which have no members that are supported by this component will not be present in this
|
||||
/// dictionary.
|
||||
/// </remarks>
|
||||
[ViewVariables] public Dictionary<string, int> DamagePerGroup = new();
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all damages in the DamageableComponent.
|
||||
/// </summary>
|
||||
[ViewVariables] public int TotalDamage;
|
||||
|
||||
// Really these shouldn't be here. OnExplosion() and RadiationAct() should be handled elsewhere.
|
||||
[ViewVariables]
|
||||
[DataField("radiationDamageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
|
||||
public List<string> RadiationDamageTypeIDs = new() {"Radiation"};
|
||||
[ViewVariables]
|
||||
[DataField("explosionDamageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
|
||||
public List<string> ExplosionDamageTypeIDs = new() { "Piercing", "Heat" };
|
||||
|
||||
// TODO RADIATION Remove this.
|
||||
void IRadiationAct.RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
|
||||
{
|
||||
var damageValue = Math.Max((int) (frameTime * radiation.RadsPerSecond), 1);
|
||||
|
||||
// Radiation should really just be a damage group instead of a list of types.
|
||||
DamageSpecifier damage = new();
|
||||
foreach (var typeID in ExplosionDamageTypeIDs)
|
||||
{
|
||||
damage.DamageDict.Add(typeID, damageValue);
|
||||
}
|
||||
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, damage);
|
||||
}
|
||||
|
||||
// TODO EXPLOSION Remove this.
|
||||
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
|
||||
{
|
||||
var damageValue = eventArgs.Severity switch
|
||||
{
|
||||
ExplosionSeverity.Light => 20,
|
||||
ExplosionSeverity.Heavy => 60,
|
||||
ExplosionSeverity.Destruction => 250,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
// Explosion should really just be a damage group instead of a list of types.
|
||||
DamageSpecifier damage = new();
|
||||
foreach (var typeID in ExplosionDamageTypeIDs)
|
||||
{
|
||||
damage.DamageDict.Add(typeID, damageValue);
|
||||
}
|
||||
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, damage);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class DamageableComponentState : ComponentState
|
||||
{
|
||||
public readonly Dictionary<string, int> DamageDict;
|
||||
public readonly string? ResistanceSetID;
|
||||
|
||||
public DamageableComponentState(
|
||||
Dictionary<string, int> damageDict,
|
||||
string? resistanceSetID)
|
||||
{
|
||||
DamageDict = damageDict;
|
||||
ResistanceSetID = resistanceSetID;
|
||||
}
|
||||
}
|
||||
}
|
||||
243
Content.Shared/Damage/DamageableSystem.cs
Normal file
243
Content.Shared/Damage/DamageableSystem.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
{
|
||||
public 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);
|
||||
}
|
||||
|
||||
/// <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, 0);
|
||||
}
|
||||
|
||||
foreach (var groupID in damageContainerPrototype.SupportedGroups)
|
||||
{
|
||||
var group = _prototypeManager.Index<DamageGroupPrototype>(groupID);
|
||||
foreach (var type in group.DamageTypes)
|
||||
{
|
||||
component.Damage.DamageDict.TryAdd(type, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
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, 0);
|
||||
}
|
||||
}
|
||||
|
||||
component.DamagePerGroup = component.Damage.GetDamagePerGroup();
|
||||
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)
|
||||
{
|
||||
component.DamagePerGroup = component.Damage.GetDamagePerGroup();
|
||||
component.TotalDamage = component.Damage.Total;
|
||||
component.Dirty();
|
||||
RaiseLocalEvent(component.Owner.Uid, new DamageChangedEvent(component, damageDelta), 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)
|
||||
{
|
||||
if (!ComponentManager.TryGetComponent<DamageableComponent>(uid, out var damageable))
|
||||
{
|
||||
// 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 && damageable.ResistanceSetID != null)
|
||||
{
|
||||
if (_prototypeManager.TryIndex<ResistanceSetPrototype>(damageable.ResistanceSetID, out var resistanceSet))
|
||||
{
|
||||
damage = DamageSpecifier.ApplyResistanceSet(damage, resistanceSet);
|
||||
}
|
||||
|
||||
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(0);
|
||||
|
||||
var delta = damageable.Damage - oldDamage;
|
||||
delta.TrimZeros();
|
||||
|
||||
if (!delta.Empty)
|
||||
{
|
||||
DamageChanged(damageable, delta);
|
||||
}
|
||||
|
||||
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, int 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());
|
||||
}
|
||||
|
||||
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new DamageableComponentState(component.Damage.DamageDict, component.ResistanceSetID);
|
||||
}
|
||||
|
||||
private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not DamageableComponentState state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
component.ResistanceSetID = state.ResistanceSetID;
|
||||
|
||||
// Has the damage actually changed?
|
||||
DamageSpecifier newDamage = new() { DamageDict = state.DamageDict };
|
||||
var delta = component.Damage - newDamage;
|
||||
delta.TrimZeros();
|
||||
|
||||
if (!delta.Empty)
|
||||
{
|
||||
component.Damage = newDamage;
|
||||
DamageChanged(component, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public 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;
|
||||
|
||||
public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta)
|
||||
{
|
||||
Damageable = damageable;
|
||||
DamageDelta = damageDelta;
|
||||
|
||||
if (DamageDelta == null)
|
||||
return;
|
||||
|
||||
foreach (var damageChange in DamageDelta.DamageDict.Values)
|
||||
{
|
||||
if (damageChange > 0)
|
||||
{
|
||||
DamageIncreased = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs
Normal file
40
Content.Shared/Damage/Prototypes/DamageContainerPrototype.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Damage.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// A damage container which can be used to specify support for various damage types.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is effectively just a list of damage types that can be specified in YAML files using both damage types
|
||||
/// and damage groups. Currently this is only used to specify what damage types a <see
|
||||
/// cref="DamageableComponent"/> should support.
|
||||
/// </remarks>
|
||||
[Prototype("damageContainer")]
|
||||
[Serializable, NetSerializable]
|
||||
public class DamageContainerPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// List of damage groups that are supported by this container.
|
||||
/// </summary>
|
||||
[DataField("supportedGroups", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageGroupPrototype>))]
|
||||
public List<string> SupportedGroups = new();
|
||||
|
||||
/// <summary>
|
||||
/// Partial List of damage types supported by this container. Note that members of the damage groups listed
|
||||
/// in <see cref="SupportedGroups"/> are also supported, but they are not included in this list.
|
||||
/// </summary>
|
||||
[DataField("supportedTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
|
||||
public List<string> SupportedTypes = new();
|
||||
}
|
||||
}
|
||||
26
Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs
Normal file
26
Content.Shared/Damage/Prototypes/DamageGroupPrototype.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Shared.Damage.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// A Group of <see cref="DamageTypePrototype"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These groups can be used to specify supported damage types of a <see cref="DamageContainerPrototype"/>, or
|
||||
/// to change/get/set damage in a <see cref="DamageableComponent"/>.
|
||||
/// </remarks>
|
||||
[Prototype("damageGroup")]
|
||||
[Serializable, NetSerializable]
|
||||
public class DamageGroupPrototype : IPrototype
|
||||
{
|
||||
[DataField("id", required: true)] public string ID { get; } = default!;
|
||||
|
||||
[DataField("damageTypes", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
|
||||
public List<string> DamageTypes { get; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
namespace Content.Shared.Damage.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// A single damage type. These types are grouped together in <see cref="DamageGroupPrototype"/>s.
|
||||
31
Content.Shared/Damage/Prototypes/ResistanceSetPrototype.cs
Normal file
31
Content.Shared/Damage/Prototypes/ResistanceSetPrototype.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Damage.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Prototype of damage resistance sets. Can be applied to <see cref="DamageSpecifier"/> using <see
|
||||
/// cref="DamageSpecifier.ApplyResistanceSet(ResistanceSetPrototype)"/>. This can be done several times as the
|
||||
/// <see cref="DamageSpecifier"/> is passed to it's final target. By default the receiving <see cref="DamageableComponent"/>, will
|
||||
/// also apply it's own <see cref="ResistanceSetPrototype"/>.
|
||||
/// </summary>
|
||||
[Prototype("resistanceSet")]
|
||||
[Serializable, NetSerializable]
|
||||
public class ResistanceSetPrototype : IPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("coefficients", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<float, DamageTypePrototype>))]
|
||||
public Dictionary<string, float> Coefficients = new();
|
||||
|
||||
[DataField("flatReductions", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<float, DamageTypePrototype>))]
|
||||
public Dictionary<string, float> FlatReduction = new();
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Damage.Resistances
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of resistances used by damageable objects.
|
||||
/// Each <see cref="DamageTypePrototype"/> has a multiplier and flat damage
|
||||
/// reduction value.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class ResistanceSet
|
||||
{
|
||||
|
||||
[ViewVariables]
|
||||
public string? ID { get; } = string.Empty;
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<DamageTypePrototype, ResistanceSetSettings> Resistances { get; } = new();
|
||||
|
||||
public ResistanceSet()
|
||||
{
|
||||
}
|
||||
|
||||
public ResistanceSet(ResistanceSetPrototype data)
|
||||
{
|
||||
ID = data.ID;
|
||||
Resistances = data.Resistances;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts input damage with the resistance set values.
|
||||
/// Only applies reduction if the amount is damage (positive), not
|
||||
/// healing (negative).
|
||||
/// </summary>
|
||||
/// <param name="damageType">Type of damage.</param>
|
||||
/// <param name="amount">Incoming amount of damage.</param>
|
||||
public int CalculateDamage(DamageTypePrototype damageType, int amount)
|
||||
{
|
||||
|
||||
// Do nothing if the damage type is not specified in resistance set.
|
||||
if (!Resistances.TryGetValue(damageType, out var resistance))
|
||||
{
|
||||
return amount;
|
||||
}
|
||||
|
||||
if (amount > 0) // Only apply reduction if it's healing, not damage.
|
||||
{
|
||||
amount -= resistance.FlatReduction;
|
||||
|
||||
if (amount <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
amount = (int) Math.Ceiling(amount * resistance.Coefficient);
|
||||
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using System.CodeDom;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Damage.Resistances
|
||||
{
|
||||
/// <summary>
|
||||
/// Prototype for the BodyPart class.
|
||||
/// </summary>
|
||||
[Prototype("resistanceSet")]
|
||||
[Serializable, NetSerializable]
|
||||
public class ResistanceSetPrototype : IPrototype, ISerializationHooks
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("coefficients", required: true)]
|
||||
private Dictionary<string, float> coefficients { get; } = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("flatReductions", required: true)]
|
||||
private Dictionary<string, int> flatReductions { get; } = new();
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<DamageTypePrototype, ResistanceSetSettings> Resistances { get; private set; } = new();
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
foreach (var damageTypeID in coefficients.Keys)
|
||||
{
|
||||
var resolvedDamageType = prototypeManager.Index<DamageTypePrototype>(damageTypeID);
|
||||
Resistances.Add(resolvedDamageType, new ResistanceSetSettings(coefficients[damageTypeID], flatReductions[damageTypeID]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resistance Settings for a specific DamageType. Flat reduction should always be applied before the coefficient.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public readonly struct ResistanceSetSettings
|
||||
{
|
||||
[ViewVariables] public readonly float Coefficient;
|
||||
[ViewVariables] public readonly int FlatReduction;
|
||||
|
||||
public ResistanceSetSettings(float coefficient, int flatReduction)
|
||||
{
|
||||
Coefficient = coefficient;
|
||||
FlatReduction = flatReduction;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user