diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 438bb87a86..2514b1deb6 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -15,6 +15,9 @@ namespace Content.Client factory.RegisterIgnore("Inventory"); factory.RegisterIgnore("Item"); factory.RegisterIgnore("Interactable"); + factory.RegisterIgnore("Damageable"); + factory.RegisterIgnore("Destructible"); + factory.RegisterIgnore("Temperature"); factory.Register(); factory.RegisterReference(); diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 1ff7d0523d..e6e25054f4 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -39,14 +39,14 @@ x64 - - - - - - - - + + + + + + + + $(SolutionDir)packages\YamlDotNet.4.2.1\lib\net35\YamlDotNet.dll @@ -55,16 +55,23 @@ - + - - - - - - - + + + + + + + + + + + + + + @@ -84,13 +91,13 @@ SS14.Shared - - - + + + - - - - + + + + - \ No newline at end of file + diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 2c9b03ec3b..1075573c64 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -23,6 +23,10 @@ namespace Content.Server factory.Register(); factory.RegisterReference(); + + factory.Register(); + factory.Register(); + factory.Register(); } } } diff --git a/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs b/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs new file mode 100644 index 0000000000..aeac75cd7b --- /dev/null +++ b/Content.Server/GameObjects/Components/Damage/DamageableComponent.cs @@ -0,0 +1,197 @@ +using Content.Server.Interfaces.GameObjects; +using System; +using System.Collections.Generic; +using OpenTK; +using SS14.Shared.GameObjects; +using SS14.Shared.Utility; +using YamlDotNet.RepresentationModel; +using Content.Server.Interfaces; +using Content.Shared.GameObjects; + +namespace Content.Server.GameObjects +{ + //TODO: add support for component add/remove + + /// + /// A component that handles receiving damage and healing, + /// as well as informing other components of it. + /// + public class DamageableComponent : Component, IDamageableComponent + { + /// + public override string Name => "Damageable"; + + /// + public override uint? NetID => ContentNetIDs.DAMAGEABLE; + + /// + /// The resistance set of this object. + /// Affects receiving damage of various types. + /// + public ResistanceSet Resistances { get; private set; } + + Dictionary CurrentDamage = new Dictionary(); + Dictionary> Thresholds = new Dictionary>(); + + public event EventHandler DamageThresholdPassed; + + /// + public override void LoadParameters(YamlMappingNode mapping) + { + if (mapping.TryGetNode("resistanceset", out YamlNode node)) + { + Resistances = ResistanceSet.GetResistanceSet(node.AsString()); + } + } + + /// + public override void Initialize() + { + base.Initialize(); + InitializeDamageType(DamageType.Total); + if (Owner is IOnDamageBehavior damageBehavior) + { + AddThresholdsFrom(damageBehavior); + } + + RecalculateComponentThresholds(); + } + + /// + public void TakeDamage(DamageType damageType, int amount) + { + if (damageType == DamageType.Total) + { + throw new ArgumentException("Cannot take damage for DamageType.Total"); + } + InitializeDamageType(damageType); + + int oldValue = CurrentDamage[damageType]; + int oldTotalValue = -1; + + if (amount == 0) + { + return; + } + + amount = Resistances.CalculateDamage(damageType, amount); + CurrentDamage[damageType] = Math.Max(0, CurrentDamage[damageType] + amount); + UpdateForDamageType(damageType, oldValue); + + if (Resistances.AppliesToTotal(damageType)) + { + oldTotalValue = CurrentDamage[DamageType.Total]; + CurrentDamage[DamageType.Total] = Math.Max(0, CurrentDamage[DamageType.Total] + amount); + UpdateForDamageType(DamageType.Total, oldTotalValue); + } + } + + /// + public void TakeHealing(DamageType damageType, int amount) + { + if (damageType == DamageType.Total) + { + throw new ArgumentException("Cannot heal for DamageType.Total"); + } + TakeDamage(damageType, -amount); + } + + void UpdateForDamageType(DamageType damageType, int oldValue) + { + int change = CurrentDamage[damageType] - oldValue; + + if (change == 0) + { + return; + } + + int changeSign = Math.Sign(change); + + foreach (int value in Thresholds[damageType]) + { + if (((value * changeSign) > (oldValue * changeSign)) && ((value * changeSign) <= (CurrentDamage[damageType] * changeSign))) + { + var args = new DamageThresholdPassedEventArgs(new DamageThreshold(damageType, value), (changeSign > 0)); + DamageThresholdPassed?.Invoke(this, args); + } + } + } + + void RecalculateComponentThresholds() + { + foreach (IOnDamageBehavior onDamageBehaviorComponent in Owner.GetComponents()) + { + AddThresholdsFrom(onDamageBehaviorComponent); + } + } + + void AddThresholdsFrom(IOnDamageBehavior onDamageBehavior) + { + if (onDamageBehavior == null) + { + throw new ArgumentNullException(nameof(onDamageBehavior)); + } + + List thresholds = onDamageBehavior.GetAllDamageThresholds(); + + foreach (DamageThreshold threshold in thresholds) + { + if (!Thresholds[threshold.DamageType].Contains(threshold.Value)) + { + Thresholds[threshold.DamageType].Add(threshold.Value); + } + } + } + + void InitializeDamageType(DamageType damageType) + { + if (!CurrentDamage.ContainsKey(damageType)) + { + CurrentDamage.Add(damageType, 0); + Thresholds.Add(damageType, new List()); + } + } + } + + public struct DamageThreshold + { + public DamageType DamageType { get; } + public int Value { get; } + + public DamageThreshold(DamageType damageType, int value) + { + DamageType = damageType; + Value = value; + } + + public override bool Equals(Object obj) + { + return obj is DamageThreshold && this == (DamageThreshold)obj; + } + public override int GetHashCode() + { + return DamageType.GetHashCode() ^ Value.GetHashCode(); + } + public static bool operator ==(DamageThreshold x, DamageThreshold y) + { + return x.DamageType == y.DamageType && x.Value == y.Value; + } + public static bool operator !=(DamageThreshold x, DamageThreshold y) + { + return !(x == y); + } + } + + public class DamageThresholdPassedEventArgs : EventArgs + { + public DamageThreshold DamageThreshold { get; } + public bool Passed { get; } + + public DamageThresholdPassedEventArgs(DamageThreshold threshold, bool passed) + { + DamageThreshold = threshold; + Passed = passed; + } + } +} + diff --git a/Content.Server/GameObjects/Components/Damage/DestructibleComponent.cs b/Content.Server/GameObjects/Components/Damage/DestructibleComponent.cs new file mode 100644 index 0000000000..8b4134734a --- /dev/null +++ b/Content.Server/GameObjects/Components/Damage/DestructibleComponent.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using SS14.Shared.GameObjects; +using SS14.Shared.Log; +using SS14.Shared.Utility; +using YamlDotNet.RepresentationModel; +using Content.Server.Interfaces; +using Content.Shared.GameObjects; + + +namespace Content.Server.GameObjects +{ + /// + /// Deletes the entity once a certain damage threshold has been reached. + /// + public class DestructibleComponent : Component, IOnDamageBehavior + { + /// + public override string Name => "Destructible"; + + /// + public override uint? NetID => ContentNetIDs.DESTRUCTIBLE; + + /// + /// Damage threshold calculated from the values + /// given in the prototype declaration. + /// + public DamageThreshold Threshold { get; private set; } + + /// + public override void LoadParameters(YamlMappingNode mapping) + { + //TODO currently only supports one threshold pair; gotta figure out YAML better + + YamlNode node; + + DamageType damageType = DamageType.Total; + int damageValue = 0; + + if (mapping.TryGetNode("thresholdtype", out node)) + { + damageType = node.AsEnum(); + } + if (mapping.TryGetNode("thresholdvalue", out node)) + { + damageValue = node.AsInt(); + } + + Threshold = new DamageThreshold(damageType, damageValue); + } + + /// + public override void Initialize() + { + base.Initialize(); + + if (Owner.TryGetComponent(out DamageableComponent damageable)) + { + damageable.DamageThresholdPassed += OnDamageThresholdPassed; + } + } + + /// + public List GetAllDamageThresholds() + { + return new List() { Threshold }; + } + + /// + public void OnDamageThresholdPassed(object obj, DamageThresholdPassedEventArgs e) + { + if (e.Passed && e.DamageThreshold == Threshold) + { + Owner.EntityManager.DeleteEntity(Owner); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Damage/ResistanceSet.cs b/Content.Server/GameObjects/Components/Damage/ResistanceSet.cs new file mode 100644 index 0000000000..dd4a860f66 --- /dev/null +++ b/Content.Server/GameObjects/Components/Damage/ResistanceSet.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; + +namespace Content.Server.GameObjects +{ + /// + /// Damage types used in-game. + /// Total should never be used directly - it's a derived value. + /// + public enum DamageType + { + Total, + Brute, + Heat, + Cold, + Acid, + Toxic, + Electric + } + + /// + /// Resistance set used by damageable objects. + /// For each damage type, has a coefficient, damage reduction and "included in total" value. + /// + public class ResistanceSet + { + Dictionary _resistances = new Dictionary(); + static Dictionary _resistanceSets = new Dictionary(); + + //TODO: make it load from YAML instead of hardcoded like this + public ResistanceSet() + { + _resistances.Add(DamageType.Total, new ResistanceSetSettings(1f, 0, true)); + _resistances.Add(DamageType.Acid, new ResistanceSetSettings(1f, 0, true)); + _resistances.Add(DamageType.Brute, new ResistanceSetSettings(1f, 0, true)); + _resistances.Add(DamageType.Heat, new ResistanceSetSettings(1f, 0, true)); + _resistances.Add(DamageType.Cold, new ResistanceSetSettings(1f, 0, true)); + _resistances.Add(DamageType.Toxic, new ResistanceSetSettings(1f, 0, true)); + _resistances.Add(DamageType.Electric, new ResistanceSetSettings(1f, 0, true)); + } + + /// + /// Loads a resistance set with the given name. + /// + /// Name of the resistance set. + /// Resistance set by given name + public static ResistanceSet GetResistanceSet(string setName) + { + ResistanceSet resistanceSet = null; + + if (!_resistanceSets.TryGetValue(setName, out resistanceSet)) + { + resistanceSet = Load(setName); + } + + return resistanceSet; + } + + static ResistanceSet Load(string setName) + { + //TODO: only creates a standard set RN, should be YAMLed + + ResistanceSet resistanceSet = new ResistanceSet(); + + _resistanceSets.Add(setName, resistanceSet); + + return resistanceSet; + } + + /// + /// Adjusts input damage with the resistance set values. + /// + /// Type of the damage. + /// Incoming amount of the damage. + /// Damage adjusted by the resistance set. + public int CalculateDamage(DamageType damageType, int amount) + { + if (amount > 0) //if it's damage, reduction applies + { + amount -= _resistances[damageType].DamageReduction; + + if (amount <= 0) + return 0; + } + + amount = (int)Math.Floor(amount * _resistances[damageType].Coefficient); + + return amount; + } + + public bool AppliesToTotal(DamageType damageType) + { + //Damage that goes straight to total (for whatever reason) never applies twice + + return damageType == DamageType.Total ? false : _resistances[damageType].AppliesToTotal; + } + + /// + /// Settings for a specific damage type in a resistance set. + /// + struct ResistanceSetSettings + { + public float Coefficient { get; private set; } + public int DamageReduction { get; private set; } + public bool AppliesToTotal { get; private set; } + + public ResistanceSetSettings(float coefficient, int damageReduction, bool appliesInTotal) + { + Coefficient = coefficient; + DamageReduction = damageReduction; + AppliesToTotal = appliesInTotal; + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Temperature/TemperatureComponent.cs b/Content.Server/GameObjects/Components/Temperature/TemperatureComponent.cs new file mode 100644 index 0000000000..557ca72ab7 --- /dev/null +++ b/Content.Server/GameObjects/Components/Temperature/TemperatureComponent.cs @@ -0,0 +1,65 @@ +using Content.Server.Interfaces.GameObjects; +using Content.Shared.Maths; +using System; +using SS14.Shared.GameObjects; +using SS14.Shared.Utility; +using YamlDotNet.RepresentationModel; +using Content.Shared.GameObjects; + +namespace Content.Server.GameObjects +{ + /// + /// Handles changing temperature, + /// informing others of the current temperature, + /// and taking fire damage from high temperature. + /// + public class TemperatureComponent : Component, ITemperatureComponent + { + /// + public override string Name => "Temperature"; + + /// + public override uint? NetID => ContentNetIDs.TEMPERATURE; + + //TODO: should be programmatic instead of how it currently is + public float CurrentTemperature { get; private set; } = PhysicalConstants.ZERO_CELCIUS; + + float _fireDamageThreshold = 0; + float _fireDamageCoefficient = 1; + + float _secondsSinceLastDamageUpdate = 0; + + /// + public override void LoadParameters(YamlMappingNode mapping) + { + YamlNode node; + + if (mapping.TryGetNode("firedamagethreshold", out node)) + { + _fireDamageThreshold = node.AsFloat(); + } + if (mapping.TryGetNode("firedamagecoefficient", out node)) + { + _fireDamageCoefficient = node.AsFloat(); + } + } + + /// + public override void Update(float frameTime) + { + base.Update(frameTime); + + int fireDamage = (int)Math.Floor(Math.Max(0, CurrentTemperature - _fireDamageThreshold) / _fireDamageCoefficient); + + _secondsSinceLastDamageUpdate += frameTime; + + Owner.TryGetComponent(out DamageableComponent component); + + while (_secondsSinceLastDamageUpdate >= 1) + { + component?.TakeDamage(DamageType.Heat, fireDamage); + _secondsSinceLastDamageUpdate -= 1; + } + } + } +} diff --git a/Content.Server/Interfaces/GameObjects/Components/Damage/IDamageableComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Damage/IDamageableComponent.cs new file mode 100644 index 0000000000..a9021feb77 --- /dev/null +++ b/Content.Server/Interfaces/GameObjects/Components/Damage/IDamageableComponent.cs @@ -0,0 +1,30 @@ +using Content.Server.GameObjects; +using SS14.Shared.Interfaces.GameObjects; +using System; + +namespace Content.Server.Interfaces.GameObjects +{ + public interface IDamageableComponent : IComponent + { + event EventHandler DamageThresholdPassed; + ResistanceSet Resistances { get; } + + /// + /// The function that handles receiving damage. + /// Converts damage via the resistance set then applies it + /// and informs components of thresholds passed as necessary. + /// + /// Type of damage being received. + /// Amount of damage being received. + void TakeDamage(DamageType damageType, int amount); + + /// + /// Handles receiving healing. + /// Converts healing via the resistance set then applies it + /// and informs components of thresholds passed as necessary. + /// + /// Type of damage being received. + /// Amount of damage being received. + void TakeHealing(DamageType damageType, int amount); + } +} diff --git a/Content.Server/Interfaces/GameObjects/Components/Temperature/ITemperatureComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Temperature/ITemperatureComponent.cs new file mode 100644 index 0000000000..64a7692801 --- /dev/null +++ b/Content.Server/Interfaces/GameObjects/Components/Temperature/ITemperatureComponent.cs @@ -0,0 +1,9 @@ +using SS14.Shared.Interfaces.GameObjects; + +namespace Content.Server.Interfaces.GameObjects +{ + public interface ITemperatureComponent : IComponent + { + float CurrentTemperature { get; } + } +} diff --git a/Content.Server/Interfaces/GameObjects/IOnDamageBehavior.cs b/Content.Server/Interfaces/GameObjects/IOnDamageBehavior.cs new file mode 100644 index 0000000000..c15da0d04c --- /dev/null +++ b/Content.Server/Interfaces/GameObjects/IOnDamageBehavior.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Content.Server.GameObjects; + +namespace Content.Server.Interfaces +{ + /// + /// Any component/entity that has behaviour linked to taking damage should implement this interface. + /// TODO: Don't know how to work around this currently, but due to how events work + /// you need to hook it up to the DamageableComponent via Initialize(). + /// See DestructibleComponent.Initialize() for an example. + /// + interface IOnDamageBehavior + { + /// + /// Gets a list of all DamageThresholds this component/entity are interested in. + /// + /// List of DamageThresholds to be added to DamageableComponent for watching. + List GetAllDamageThresholds(); + + /// + /// Damage threshold passed event hookup. + /// + /// Damageable component. + /// Damage threshold and whether it's passed in one way or another. + void OnDamageThresholdPassed(object obj, DamageThresholdPassedEventArgs e); + } +} diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj index c200d1c88f..bd366e7ba7 100644 --- a/Content.Shared/Content.Shared.csproj +++ b/Content.Shared/Content.Shared.csproj @@ -55,9 +55,11 @@ + + - + @@ -97,4 +99,4 @@ - \ No newline at end of file + diff --git a/Content.Shared/GameObjects/Components/NetIDs.cs b/Content.Shared/GameObjects/Components/NetIDs.cs deleted file mode 100644 index fe77227233..0000000000 --- a/Content.Shared/GameObjects/Components/NetIDs.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.GameObjects -{ - public static class ContentNetIDs - { - public const uint HANDS = 1000; - } -} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs new file mode 100644 index 0000000000..9d67fe9038 --- /dev/null +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.GameObjects +{ + // Starting from 1000 to avoid crossover with engine. + public static class ContentNetIDs + { + public const uint DAMAGEABLE = 1000; + public const uint DESTRUCTIBLE = 1001; + public const uint TEMPERATURE = 1002; + public const uint HANDS = 1003; + } +} diff --git a/Content.Shared/GameObjects/PhysicalConstants.cs b/Content.Shared/GameObjects/PhysicalConstants.cs new file mode 100644 index 0000000000..bbe806ff0e --- /dev/null +++ b/Content.Shared/GameObjects/PhysicalConstants.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.GameObjects +{ + /// + /// Contains physical constants used in calculations. + /// + class PhysicalConstants + { + public const float ZERO_CELCIUS = 273.15f; + } +} diff --git a/Content.Shared/Maths/PhysicalConstants.cs b/Content.Shared/Maths/PhysicalConstants.cs new file mode 100644 index 0000000000..301861a0eb --- /dev/null +++ b/Content.Shared/Maths/PhysicalConstants.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.Maths +{ + /// + /// Contains physical constants used in calculations. + /// + public static class PhysicalConstants + { + public const float ZERO_CELCIUS = 273.15f; + } +} diff --git a/Resources/Prototypes/1_Temperature.yml b/Resources/Prototypes/1_Temperature.yml new file mode 100644 index 0000000000..f3c69ed49d --- /dev/null +++ b/Resources/Prototypes/1_Temperature.yml @@ -0,0 +1,23 @@ +- type: entity + id: thing_that_heats_up_on_its_own_and_dies + name: Thing that heats up on its own and dies + components: + - type: Transform + - type: Clickable + - type: Sprite + sprites: + - shoes + + - type: Icon + icon: shoes + + - type: Damageable + resistanceset: Standard + + - type: Destructible + thresholdtype: Total + thresholdvalue: 100 + + - type: Temperature + firedamagethreshold: 200 + firedamagecoefficient: 20