diff --git a/Content.Client/MedicalScanner/UI/MedicalScannerWindow.cs b/Content.Client/MedicalScanner/UI/MedicalScannerWindow.cs index 2c42dc5fb9..58aaeafa93 100644 --- a/Content.Client/MedicalScanner/UI/MedicalScannerWindow.cs +++ b/Content.Client/MedicalScanner/UI/MedicalScannerWindow.cs @@ -1,10 +1,13 @@ using System.Text; +using System.Collections.Generic; using Content.Shared.Damage; +using System.Linq; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Prototypes; using static Content.Shared.MedicalScanner.SharedMedicalScannerComponent; using static Robust.Client.UserInterface.Controls.BoxContainer; @@ -14,6 +17,7 @@ namespace Content.Client.MedicalScanner.UI { public readonly Button ScanButton; private readonly Label _diagnostics; + public MedicalScannerWindow() { SetSize = (250, 100); @@ -51,27 +55,65 @@ namespace Content.Client.MedicalScanner.UI { text.Append($"{Loc.GetString("medical-scanner-window-entity-health-text", ("entityName", entity.Name))}\n"); - foreach (var (@class, classAmount) in state.DamageClasses) + // Show the total damage + var totalDamage = state.DamagePerTypeID.Values.Sum(); + text.Append($"{Loc.GetString("medical-scanner-window-entity-damage-total-text", ("amount", totalDamage))}\n"); + + // Keep track of how many damage types we have shown + HashSet shownTypeIDs = new(); + + // First show just the total damage and type breakdown for each damge group that is fully supported by that entitygroup. + foreach (var (damageGroupID, damageAmount) in state.DamagePerSupportedGroupID) { - text.Append($"\n{Loc.GetString("medical-scanner-window-damage-class-text", ("damageClass", @class), ("amount", classAmount))}"); - foreach (var type in @class.ToTypes()) + // Show total damage for the group + text.Append($"\n{Loc.GetString("medical-scanner-window-damage-group-text", ("damageGroup", damageGroupID), ("amount", damageAmount))}"); + + // Then show the damage for each type in that group. + // currently state has a dictionary mapping groupsIDs to damage, and typeIDs to damage, but does not know how types and groups are related. + // So use PrototypeManager. + var group = IoCManager.Resolve().Index(damageGroupID); + foreach (var type in group.DamageTypes) { - if (!state.DamageTypes.TryGetValue(type, out var typeAmount)) + if (state.DamagePerTypeID.TryGetValue(type.ID, out var typeAmount)) { - continue; + // If damage types are allowed to belong to more than one damage group, they may appear twice here. Mark them as duplicate. + if (!shownTypeIDs.Contains(type.ID)) + { + shownTypeIDs.Add(type.ID); + text.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-text", ("damageType", type.ID), ("amount", typeAmount))}"); + } + else { + text.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-duplicate-text", ("damageType", type.ID), ("amount", typeAmount))}"); + } } - - text.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-text", ("damageType",type) ,("amount", typeAmount))}"); } - text.Append('\n'); } + // Then, lets also list any damageType that was not fully Supported by the entity's damageContainer + var textAppendix = new StringBuilder(); + int totalMiscDamage = 0; + // Iterate over ids that have not been printed. + foreach (var damageTypeID in state.DamagePerTypeID.Keys.Where(typeID => !shownTypeIDs.Contains(typeID))) + { + //This damage type was not yet added to the text. + textAppendix.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-text", ("damageType", damageTypeID), ("amount", state.DamagePerTypeID[damageTypeID]))}"); + totalMiscDamage += state.DamagePerTypeID[damageTypeID]; + } + + // Is there any information to show? Did any damage types not belong to a group? + if (textAppendix.Length > 0) { + text.Append($"\n{Loc.GetString("medical-scanner-window-damage-group-text", ("damageGroup", "Miscellaneous"), ("amount", totalMiscDamage))}"); + text.Append(textAppendix); + } + _diagnostics.Text = text.ToString(); ScanButton.Disabled = state.IsScanned; - SetSize = (250, 575); + // TODO MEDICALSCANNER resize window based on the length of text / number of damage types? + // Also, maybe add color schemes for specific damage groups? + SetSize = (250, 600); } } } diff --git a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs index 1f9852a6af..f5b9fc5294 100644 --- a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs +++ b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Content.Server.Damage; using Content.Shared.Damage; using Content.Shared.Damage.Components; @@ -7,6 +7,7 @@ using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Commands { @@ -20,7 +21,7 @@ namespace Content.IntegrationTests.Tests.Commands id: DamageableDummy components: - type: Damageable - damagePrototype: biologicalDamageContainer + damageContainer: biologicalDamageContainer - type: MobState thresholds: 0: !type:NormalMobState {} @@ -41,6 +42,7 @@ namespace Content.IntegrationTests.Tests.Commands mapManager.CreateNewMapEntity(MapId.Nullspace); var entityManager = IoCManager.Resolve(); + var prototypeManager = IoCManager.Resolve(); var human = entityManager.SpawnEntity("DamageableDummy", MapCoordinates.Nullspace); @@ -53,7 +55,7 @@ namespace Content.IntegrationTests.Tests.Commands Assert.That(mobState.IsIncapacitated, Is.False); // Kill the entity - damageable.ChangeDamage(DamageClass.Brute, 10000000, true); + damageable.TryChangeDamage(prototypeManager.Index("Toxin"), 10000000, true); // Check that it is dead Assert.That(mobState.IsAlive, Is.False); diff --git a/Content.IntegrationTests/Tests/Damageable/AllSupportDamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/AllSupportDamageableTest.cs new file mode 100644 index 0000000000..97db32a968 --- /dev/null +++ b/Content.IntegrationTests/Tests/Damageable/AllSupportDamageableTest.cs @@ -0,0 +1,121 @@ +using System.Linq; +using System.Threading.Tasks; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests.Damageable +{ + [TestFixture] + [TestOf(typeof(DamageableComponent))] + public class AllSupportDamageableTest : ContentIntegrationTest + { + private const string AllDamageDamageableEntityId = "TestAllDamageDamageableEntityId"; + + /// + /// Test a damageContainer with all types supported. + /// + /// + /// As this should also loads in the damage groups & types in the actual damage.yml, this should also act as a basic test to see if damage.yml is set up properly. + /// + [Test] + public async Task TestAllSupportDamageableComponent() + { + var server = StartServerDummyTicker(); + await server.WaitIdleAsync(); + + var sEntityManager = server.ResolveDependency(); + var sMapManager = server.ResolveDependency(); + var sPrototypeManager = server.ResolveDependency(); + + IEntity sFullyDamageableEntity; + IDamageableComponent sFullyDamageableComponent = null; + + await server.WaitPost(() => + { + var mapId = sMapManager.NextMapId(); + var coordinates = new MapCoordinates(0, 0, mapId); + sMapManager.CreateMap(mapId); + + // When prototypes are loaded using the ExtraPrototypes option, they seem to be loaded first? + // Or at least, no damage prototypes were loaded in by the time that the damageContainer here is loaded. + // So for now doing explicit loading of prototypes. + // I have no idea what I am doing, but it works. + sPrototypeManager.LoadString($@" +# we want to test the all damage container +- type: damageContainer + id: testAllDamageContainer + supportAll: true + +# create entities +- type: entity + id: {AllDamageDamageableEntityId} + name: {AllDamageDamageableEntityId} + components: + - type: Damageable + damageContainer: testAllDamageContainer +"); + + sFullyDamageableEntity = sEntityManager.SpawnEntity(AllDamageDamageableEntityId, coordinates); + sFullyDamageableComponent = sFullyDamageableEntity.GetComponent(); + + }); + + await server.WaitRunTicks(5); + + await server.WaitAssertion(() => + { + + // First check that there actually are any damage types/groups + // This test depends on a non-empty damage.yml + Assert.That(sPrototypeManager.EnumeratePrototypes().ToList().Count, Is.GreaterThan(0)); + Assert.That(sPrototypeManager.EnumeratePrototypes().ToList().Count, Is.GreaterThan(0)); + + + // Can we set and get all damage. + Assert.That(sFullyDamageableComponent.TrySetAllDamage(-10), Is.False); + Assert.That(sFullyDamageableComponent.TrySetAllDamage(0), Is.True); + + // Test that the all damage container supports every damage type, and that we can get, set, and change + // every type with the expected results. Notable: if the damage does not change, they all return false + var initialDamage = 10; + foreach (var damageType in sPrototypeManager.EnumeratePrototypes()) + { + var damage = initialDamage; + Assert.That(sFullyDamageableComponent.IsSupportedDamageType(damageType)); + Assert.That(sFullyDamageableComponent.TrySetDamage(damageType, -damage), Is.False); + Assert.That(sFullyDamageableComponent.TrySetDamage(damageType, damage), Is.True); + Assert.That(sFullyDamageableComponent.TrySetDamage(damageType, damage), Is.True); // intentional duplicate + Assert.That(sFullyDamageableComponent.GetDamage(damageType), Is.EqualTo(damage)); + Assert.That(sFullyDamageableComponent.TryChangeDamage(damageType, -damage / 2, true), Is.True); + Assert.That(sFullyDamageableComponent.TryGetDamage(damageType, out damage), Is.True); + Assert.That(damage, Is.EqualTo(initialDamage/2)); + Assert.That(sFullyDamageableComponent.TryChangeDamage(damageType, damage, true), Is.True); + Assert.That(sFullyDamageableComponent.GetDamage(damageType), Is.EqualTo(2* damage)); + Assert.That(sFullyDamageableComponent.TryChangeDamage(damageType, 0, true), Is.False); + } + // And again, for every group + foreach (var damageGroup in sPrototypeManager.EnumeratePrototypes()) + { + var damage = initialDamage; + var groupSize = damageGroup.DamageTypes.Count(); + Assert.That(sFullyDamageableComponent.IsFullySupportedDamageGroup(damageGroup)); + Assert.That(sFullyDamageableComponent.IsApplicableDamageGroup(damageGroup)); + Assert.That(sFullyDamageableComponent.TrySetDamage(damageGroup, -damage), Is.False); + Assert.That(sFullyDamageableComponent.TrySetDamage(damageGroup, damage), Is.True); + Assert.That(sFullyDamageableComponent.TrySetDamage(damageGroup, damage), Is.True); // intentional duplicate + Assert.That(sFullyDamageableComponent.GetDamage(damageGroup), Is.EqualTo(damage * groupSize)); + Assert.That(sFullyDamageableComponent.TryChangeDamage(damageGroup, -groupSize*damage / 2, true), Is.True); + Assert.That(sFullyDamageableComponent.TryGetDamage(damageGroup, out damage), Is.True); + Assert.That(damage, Is.EqualTo(groupSize* initialDamage/2)); + Assert.That(sFullyDamageableComponent.TryChangeDamage(damageGroup, damage, true), Is.True); + Assert.That(sFullyDamageableComponent.GetDamage(damageGroup), Is.EqualTo(2*damage)); + Assert.That(sFullyDamageableComponent.TryChangeDamage(damageGroup, 0, true), Is.False); + } + }); + } + } +} diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs index 9e01a4e2ec..f432ee559e 100644 --- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs +++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs @@ -1,10 +1,11 @@ -using System; +using System.Linq; using System.Threading.Tasks; using Content.Shared.Damage; using Content.Shared.Damage.Components; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Map; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Damageable { @@ -12,18 +13,75 @@ namespace Content.IntegrationTests.Tests.Damageable [TestOf(typeof(DamageableComponent))] public class DamageableTest : ContentIntegrationTest { - private const string DamageableEntityId = "DamageableEntityId"; + private const string DamageableEntityId = "TestDamageableEntityId"; + private const string Group1Id = "TestGroup1"; + private const string Group2Id = "TestGroup2"; + private const string Group3Id = "TestGroup3"; + private string Prototypes = $@" +# Define some damage groups +- type: damageType + id: TestDamage11 + +- type: damageType + id: TestDamage21 + +- type: damageType + id: TestDamage22 + +- type: damageType + id: TestDamage31 + +- type: damageType + id: TestDamage32 + +- type: damageType + id: TestDamage33 + +# Define damage Groups with 1,2,3 damage types +- type: damageGroup + id: {Group1Id} + damageTypes: + - TestDamage11 + +- type: damageGroup + id: {Group2Id} + damageTypes: + - TestDamage21 + - TestDamage22 + +- type: damageGroup + id: {Group3Id} + damageTypes: + - TestDamage31 + - TestDamage32 + - TestDamage33 + +# we want to test a container that supports only full groups +# we will also give full support for group 2 IMPLICITLY by specifying all of its members are supported. +- type: damageContainer + id: testSomeDamageContainer + supportedGroups: + - {Group3Id} + supportedTypes: + - TestDamage21 + - TestDamage22 - private static readonly string Prototypes = $@" - type: entity id: {DamageableEntityId} name: {DamageableEntityId} components: - type: Damageable - damageContainer: allDamageContainer"; + damageContainer: testSomeDamageContainer +"; + /// + /// Test a standard damageable components + /// + /// + /// Only test scenarios where each damage type is a member of exactly one group, and all damageable components support whole groups, not lone damage types. + /// [Test] - public async Task TestDamageTypeDamageAndHeal() + public async Task TestDamageableComponents() { var server = StartServerDummyTicker(new ServerContentIntegrationOption { @@ -34,10 +92,15 @@ namespace Content.IntegrationTests.Tests.Damageable var sEntityManager = server.ResolveDependency(); var sMapManager = server.ResolveDependency(); + var sPrototypeManager = server.ResolveDependency(); IEntity sDamageableEntity; IDamageableComponent sDamageableComponent = null; + DamageGroupPrototype group1 = default!; + DamageGroupPrototype group2 = default!; + DamageGroupPrototype group3 = default!; + await server.WaitPost(() => { var mapId = sMapManager.NextMapId(); @@ -46,123 +109,280 @@ namespace Content.IntegrationTests.Tests.Damageable sDamageableEntity = sEntityManager.SpawnEntity(DamageableEntityId, coordinates); sDamageableComponent = sDamageableEntity.GetComponent(); + + group1 = sPrototypeManager.Index(Group1Id); + group2 = sPrototypeManager.Index(Group2Id); + group3 = sPrototypeManager.Index(Group3Id); + }); await server.WaitRunTicks(5); await server.WaitAssertion(() => { - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); + // Check that the correct groups are supported by the container + Assert.That(sDamageableComponent.IsApplicableDamageGroup(group1), Is.False); + Assert.That(sDamageableComponent.IsApplicableDamageGroup(group2), Is.True); + Assert.That(sDamageableComponent.IsApplicableDamageGroup(group3), Is.True); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group1), Is.False); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group2), Is.True); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group3), Is.True); - var damageToDeal = 7; - - foreach (var type in Enum.GetValues()) + // Check that the correct types are supported: + foreach (var group in sPrototypeManager.EnumeratePrototypes()) { - Assert.That(sDamageableComponent.SupportsDamageType(type)); - - // Damage - Assert.That(sDamageableComponent.ChangeDamage(type, damageToDeal, true), Is.True); - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal)); - Assert.That(sDamageableComponent.TryGetDamage(type, out var damage), Is.True); - Assert.That(damage, Is.EqualTo(damageToDeal)); - - // Heal - Assert.That(sDamageableComponent.ChangeDamage(type, -damageToDeal, true), Is.True); - Assert.That(sDamageableComponent.TotalDamage, Is.Zero); - Assert.That(sDamageableComponent.TryGetDamage(type, out damage), Is.True); - Assert.That(damage, Is.Zero); + foreach(var type in group.DamageTypes) + { + if (sDamageableComponent.IsFullySupportedDamageGroup(group)) + { + Assert.That(sDamageableComponent.IsSupportedDamageType(type), Is.True); + } + else + { + Assert.That(sDamageableComponent.IsSupportedDamageType(type), Is.False); + } + } } - }); - } - [Test] - public async Task TestDamageClassDamageAndHeal() - { - var server = StartServerDummyTicker(new ServerContentIntegrationOption - { - ExtraPrototypes = Prototypes - }); - await server.WaitIdleAsync(); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group1), Is.False); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group2), Is.True); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group3), Is.True); - var sEntityManager = server.ResolveDependency(); - var sMapManager = server.ResolveDependency(); - - IEntity sDamageableEntity; - IDamageableComponent sDamageableComponent = null; - - await server.WaitPost(() => - { - var mapId = sMapManager.NextMapId(); - var coordinates = new MapCoordinates(0, 0, mapId); - sMapManager.CreateMap(mapId); - - sDamageableEntity = sEntityManager.SpawnEntity(DamageableEntityId, coordinates); - sDamageableComponent = sDamageableEntity.GetComponent(); - }); - - await server.WaitRunTicks(5); - - await server.WaitAssertion(() => - { + // Check that damage works properly if perfectly divisible among group members + int damageToDeal, groupDamage, typeDamage; ; Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); - - foreach (var @class in Enum.GetValues()) + foreach (var damageGroup in sDamageableComponent.FullySupportedDamageGroups) { - Assert.That(sDamageableComponent.SupportsDamageClass(@class)); - - var types = @class.ToTypes(); - - foreach (var type in types) - { - Assert.That(sDamageableComponent.SupportsDamageType(type)); - } - - var damageToDeal = types.Count * 5; + var types = damageGroup.DamageTypes; // Damage - Assert.That(sDamageableComponent.ChangeDamage(@class, damageToDeal, true), Is.True); + damageToDeal = types.Count() * 5; + Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.True); Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal)); - Assert.That(sDamageableComponent.TryGetDamage(@class, out var classDamage), Is.True); - Assert.That(classDamage, Is.EqualTo(damageToDeal)); + Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True); + Assert.That(groupDamage, Is.EqualTo(damageToDeal)); foreach (var type in types) { - Assert.That(sDamageableComponent.TryGetDamage(type, out var typeDamage), Is.True); - Assert.That(typeDamage, Is.EqualTo(damageToDeal / types.Count)); + Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True); + Assert.That(typeDamage, Is.EqualTo(damageToDeal / types.Count())); } // Heal - Assert.That(sDamageableComponent.ChangeDamage(@class, -damageToDeal, true), Is.True); + Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, -damageToDeal, true), Is.True); Assert.That(sDamageableComponent.TotalDamage, Is.Zero); - Assert.That(sDamageableComponent.TryGetDamage(@class, out classDamage), Is.True); - Assert.That(classDamage, Is.Zero); + Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True); + Assert.That(groupDamage, Is.Zero); foreach (var type in types) { - Assert.That(sDamageableComponent.TryGetDamage(type, out var typeDamage), Is.True); + Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True); Assert.That(typeDamage, Is.Zero); } } + + // Check that damage works properly if it is NOT perfectly divisible among group members + foreach (var damageGroup in sDamageableComponent.FullySupportedDamageGroups) + { + var types = damageGroup.DamageTypes; + + // Damage + damageToDeal = types.Count() * 5 - 1; + Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.True); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal)); + Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True); + Assert.That(groupDamage, Is.EqualTo(damageToDeal)); + + foreach (var type in types) + { + Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True); + float targetDamage = ((float) damageToDeal) / types.Count(); + Assert.That(typeDamage, Is.InRange(targetDamage - 1, targetDamage + 1)); + } + + // Heal + Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, -damageToDeal, true), Is.True); + Assert.That(sDamageableComponent.TotalDamage, Is.Zero); + Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True); + Assert.That(groupDamage, Is.Zero); + + foreach (var type in types) + { + Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True); + Assert.That(typeDamage, Is.Zero); + } + } + + // Test that unsupported groups return false when setting/getting damage (and don't change damage) + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); + foreach (var damageGroup in sPrototypeManager.EnumeratePrototypes()) + { + if (sDamageableComponent.IsFullySupportedDamageGroup(damageGroup)) + { + continue; + } + + Assert.That(sDamageableComponent.IsApplicableDamageGroup(damageGroup), Is.False); + + var types = damageGroup.DamageTypes; + damageToDeal = types.Count() * 5; + + foreach (var type in types) + { + Assert.That(sDamageableComponent.IsSupportedDamageType(type), Is.False); + } +; + Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.False); + Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.False); + + foreach (var type in types) + { + Assert.That(sDamageableComponent.TryChangeDamage(type, damageToDeal, true), Is.False); + Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.False); + } + } + // Did damage change? + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); + + + // Test total damage function + damageToDeal = 10; + + Assert.True(sDamageableComponent.TryChangeDamage(group3, damageToDeal, true)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal)); + + var totalTypeDamage = 0; + + foreach (var damageType in sDamageableComponent.SupportedDamageTypes) + { + Assert.True(sDamageableComponent.TryGetDamage(damageType, out typeDamage)); + Assert.That(typeDamage, Is.LessThanOrEqualTo(damageToDeal)); + + totalTypeDamage += typeDamage; + } + Assert.That(totalTypeDamage, Is.EqualTo(damageToDeal)); + + + // Test healing all damage + Assert.That(sDamageableComponent.TrySetAllDamage(0)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); + + // Test preferential healing + damageToDeal = 12; + var damageTypes = group3.DamageTypes.ToArray(); + + // Deal damage + Assert.True(sDamageableComponent.TryChangeDamage(damageTypes[0], 17)); + Assert.True(sDamageableComponent.TryChangeDamage(damageTypes[1], 31)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(48)); + + // Heal group damage + Assert.True(sDamageableComponent.TryChangeDamage(group3, -11)); + + // Check healing (3 + 9) + Assert.That(sDamageableComponent.GetDamage(damageTypes[0]), Is.EqualTo(14)); + Assert.That(sDamageableComponent.GetDamage(damageTypes[1]), Is.EqualTo(23)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(37)); + + // Heal group damage + Assert.True(sDamageableComponent.TryChangeDamage(group3, -36)); + + // Check healing (13 + 23) + Assert.That(sDamageableComponent.GetDamage(damageTypes[0]), Is.EqualTo(1)); + Assert.That(sDamageableComponent.GetDamage(damageTypes[1]), Is.EqualTo(0)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(1)); + + //Check Damage + Assert.True(sDamageableComponent.TryGetDamage(damageTypes[0], out typeDamage)); + Assert.That(typeDamage, Is.LessThanOrEqualTo(damageToDeal)); }); } + + private const string SharedDamageTypeId = "TestSharedDamage"; + private const string UnsupportedDamageTypeId = "TestUnsupportedDamage"; + private string Prototypes2 = $@" +- type: damageType + id: {SharedDamageTypeId} + +- type: damageType + id: {UnsupportedDamageTypeId} + +- type: damageType + id: TestDamage1 + +- type: damageType + id: TestDamage2 + +- type: damageGroup + id: {Group1Id} + damageTypes: + - {SharedDamageTypeId} + +- type: damageGroup + id: {Group2Id} + damageTypes: + - {SharedDamageTypeId} + - TestDamage1 + +- type: damageGroup + id: {Group3Id} + damageTypes: + - {SharedDamageTypeId} + - TestDamage2 + - {UnsupportedDamageTypeId} + +# we want to test a container that only partially supports a group: +- type: damageContainer + id: TestPartiallySupported + supportedGroups: + - {Group2Id} + supportedTypes: + - TestDamage2 + - TestDamage1 +# does NOT support type {UnsupportedDamageTypeId}, and thus does not fully support group {Group3Id} +# TestDamage1 is added twice because it is also in {Group2Id}. This should not cause errors. + +# create entities +- type: entity + id: {DamageableEntityId} + name: {DamageableEntityId} + components: + - type: Damageable + damageContainer: TestPartiallySupported +"; + + /// + /// Generalized damageable component tests. + /// + /// + /// Test scenarios where damage types are members of more than one group, or where a component only supports a subset of a group. + /// [Test] - public async Task TotalDamageTest() + public async Task TestGeneralizedDamageableComponent() { var server = StartServerDummyTicker(new ServerContentIntegrationOption { - ExtraPrototypes = Prototypes + ExtraPrototypes = Prototypes2 }); await server.WaitIdleAsync(); var sEntityManager = server.ResolveDependency(); var sMapManager = server.ResolveDependency(); + var sPrototypeManager = server.ResolveDependency(); IEntity sDamageableEntity; IDamageableComponent sDamageableComponent = null; + DamageGroupPrototype group1 = default!; + DamageGroupPrototype group2 = default!; + DamageGroupPrototype group3 = default!; + + DamageTypePrototype SharedDamageType = default!; + DamageTypePrototype UnsupportedDamageType = default!; + await server.WaitPost(() => { var mapId = sMapManager.NextMapId(); @@ -171,27 +391,62 @@ namespace Content.IntegrationTests.Tests.Damageable sDamageableEntity = sEntityManager.SpawnEntity(DamageableEntityId, coordinates); sDamageableComponent = sDamageableEntity.GetComponent(); + + group1 = sPrototypeManager.Index(Group1Id); + group2 = sPrototypeManager.Index(Group2Id); + group3 = sPrototypeManager.Index(Group3Id); + + SharedDamageType = sPrototypeManager.Index(SharedDamageTypeId); + UnsupportedDamageType = sPrototypeManager.Index(UnsupportedDamageTypeId); }); + await server.WaitRunTicks(5); + await server.WaitAssertion(() => { - var damageType = DamageClass.Brute; - var damage = 10; + // All damage types should be applicable + Assert.That(sDamageableComponent.IsApplicableDamageGroup(group1), Is.True); + Assert.That(sDamageableComponent.IsApplicableDamageGroup(group2), Is.True); + Assert.That(sDamageableComponent.IsApplicableDamageGroup(group3), Is.True); - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, damage, true)); - Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(10)); + // But not all should be fully supported + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group1), Is.True); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group2), Is.True); + Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group3), Is.False); - var totalTypeDamage = 0; + // Check that the correct damage types are supported + Assert.That(sDamageableComponent.IsSupportedDamageType(SharedDamageType), Is.True); - foreach (var type in damageType.ToTypes()) - { - Assert.True(sDamageableComponent.TryGetDamage(type, out var typeDamage)); - Assert.That(typeDamage, Is.LessThanOrEqualTo(damage)); + // Check that if we deal damage using a type appearing in multiple groups, nothing goes wrong. + var damage = 12; + Assert.That(sDamageableComponent.TryChangeDamage(SharedDamageType, damage), Is.True); + Assert.That(sDamageableComponent.GetDamage(SharedDamageType), Is.EqualTo(damage)); + Assert.That(sDamageableComponent.GetDamage(group1), Is.EqualTo(damage)); + Assert.That(sDamageableComponent.GetDamage(group2), Is.EqualTo(damage)); + Assert.That(sDamageableComponent.GetDamage(group3), Is.EqualTo(damage)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damage)); - totalTypeDamage += typeDamage; - } + // Check that if we deal damage using a group that is not fully supported, the damage is reduced + // Note that if damage2 were not neatly divisible by 3, the actual damage reduction would be subject to integer rounding. + // How much exactly the damage gets reduced then would depend on the order that the groups were defined in the yaml file + // Here we deal 9 damage. It should apply 3 damage to each type, but one type is ignored, resulting in 6 total damage. + // However, the damage in group2 and group3 only changes because of one type that overlaps, so they only change by 3 + Assert.That(sDamageableComponent.TryChangeDamage(group3, 9), Is.True); + Assert.That(sDamageableComponent.GetDamage(group1), Is.EqualTo(damage + 3)); + Assert.That(sDamageableComponent.GetDamage(group2), Is.EqualTo(damage + 3)); + Assert.That(sDamageableComponent.GetDamage(group3), Is.EqualTo(damage + 6)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damage + 6)); + + // Now we check that when healing, no damage is wasted. + // Because SharedDamageType has the most damage in group3 (15 vs 3), it will be healed more than the other. + // Expect that, up to integer rounding, one is healed 5* more than the other. + // We will use a number that does not divide nicely, there will be some integer rounding. + Assert.That(sDamageableComponent.TryChangeDamage(group3, -7), Is.True); + Assert.That(sDamageableComponent.GetDamage(group1), Is.EqualTo(damage + 3 - 5)); + Assert.That(sDamageableComponent.GetDamage(group2), Is.EqualTo(damage + 3 - 5)); + Assert.That(sDamageableComponent.GetDamage(group3), Is.EqualTo(damage + 6 - 7)); + Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damage + 6 - 7)); - Assert.That(totalTypeDamage, Is.EqualTo(damage)); }); } } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageClassTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs similarity index 72% rename from Content.IntegrationTests/Tests/Destructible/DestructibleDamageClassTest.cs rename to Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs index f335b402a2..a8c735e64b 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageClassTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Content.Server.Destructible.Thresholds.Triggers; using Content.Shared.Damage; using Content.Shared.Damage.Components; @@ -6,14 +6,15 @@ using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; namespace Content.IntegrationTests.Tests.Destructible { [TestFixture] - [TestOf(typeof(DamageClassTrigger))] + [TestOf(typeof(DamageGroupTrigger))] [TestOf(typeof(AndTrigger))] - public class DestructibleDamageClassTest : ContentIntegrationTest + public class DestructibleDamageGroupTest : ContentIntegrationTest { [Test] public async Task AndTest() @@ -31,6 +32,7 @@ namespace Content.IntegrationTests.Tests.Destructible var sEntityManager = server.ResolveDependency(); var sMapManager = server.ResolveDependency(); + var sPrototypeManager = server.ResolveDependency(); IEntity sDestructibleEntity; IDamageableComponent sDamageableComponent = null; @@ -42,7 +44,7 @@ namespace Content.IntegrationTests.Tests.Destructible var coordinates = new MapCoordinates(0, 0, mapId); sMapManager.CreateMap(mapId); - sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageClassEntityId, coordinates); + sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageGroupEntityId, coordinates); sDamageableComponent = sDestructibleEntity.GetComponent(); sThresholdListenerComponent = sDestructibleEntity.GetComponent(); }); @@ -56,20 +58,23 @@ namespace Content.IntegrationTests.Tests.Destructible await server.WaitAssertion(() => { + var bruteDamageGroup = sPrototypeManager.Index("TestBrute"); + var burnDamageGroup = sPrototypeManager.Index("TestBurn"); + // Raise brute damage to 5 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, 5, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 5, true)); // No thresholds reached yet, the earliest one is at 10 damage Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise brute damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, 5, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 5, true)); // No threshold reached, burn needs to be 10 as well Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise burn damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Burn, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true)); // One threshold reached, brute 10 + burn 10 Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -86,52 +91,52 @@ namespace Content.IntegrationTests.Tests.Destructible var trigger = (AndTrigger) threshold.Trigger; - Assert.IsInstanceOf(trigger.Triggers[0]); - Assert.IsInstanceOf(trigger.Triggers[1]); + Assert.IsInstanceOf(trigger.Triggers[0]); + Assert.IsInstanceOf(trigger.Triggers[1]); sThresholdListenerComponent.ThresholdsReached.Clear(); // Raise brute damage to 20 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise burn damage to 20 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Burn, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Lower brute damage to 0 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, -20, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -20, true)); // No new thresholds reached, healing should not trigger it Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise brute damage back up to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true)); - // 10 brute + 10 burn threshold reached, brute was healed and brought back to its threshold amount and slash stayed the same + // 10 brute + 10 burn threshold reached, brute was healed and brought back to its threshold amount and burn stayed the same Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); sThresholdListenerComponent.ThresholdsReached.Clear(); // Heal both classes of damage to 0 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, -10, true)); - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Burn, -20, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, -20, true)); // No new thresholds reached, healing should not trigger it Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise brute damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise burn damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Burn, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true)); // Both classes of damage were healed and then raised again, the threshold should have been reached as triggers once is default false Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -148,8 +153,8 @@ namespace Content.IntegrationTests.Tests.Destructible trigger = (AndTrigger) threshold.Trigger; - Assert.IsInstanceOf(trigger.Triggers[0]); - Assert.IsInstanceOf(trigger.Triggers[1]); + Assert.IsInstanceOf(trigger.Triggers[0]); + Assert.IsInstanceOf(trigger.Triggers[1]); sThresholdListenerComponent.ThresholdsReached.Clear(); @@ -157,20 +162,20 @@ namespace Content.IntegrationTests.Tests.Destructible threshold.TriggersOnce = true; // Heal brute and burn back to 0 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, -10, true)); - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Burn, -10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, -10, true)); // No new thresholds reached from healing Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise brute damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise burn damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Burn, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true)); // No new thresholds reached as triggers once is set to true and it already triggered before Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs index 4a7c2a4ee8..2868869de1 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Content.Server.Destructible.Thresholds.Triggers; using Content.Shared.Damage; using Content.Shared.Damage.Components; @@ -6,6 +6,7 @@ using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; namespace Content.IntegrationTests.Tests.Destructible @@ -56,20 +57,23 @@ namespace Content.IntegrationTests.Tests.Destructible await server.WaitAssertion(() => { + var bluntDamageType = IoCManager.Resolve().Index("TestBlunt"); + var slashDamageType = IoCManager.Resolve().Index("TestSlash"); + // Raise blunt damage to 5 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 5, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 5, true)); // No thresholds reached yet, the earliest one is at 10 damage Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise blunt damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 5, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 5, true)); // No threshold reached, slash needs to be 10 as well Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise slash damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Slash, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true)); // One threshold reached, blunt 10 + slash 10 Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -92,25 +96,25 @@ namespace Content.IntegrationTests.Tests.Destructible sThresholdListenerComponent.ThresholdsReached.Clear(); // Raise blunt damage to 20 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise slash damage to 20 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Slash, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Lower blunt damage to 0 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, -20, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -20, true)); // No new thresholds reached, healing should not trigger it Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise blunt damage back up to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); // 10 blunt + 10 slash threshold reached, blunt was healed and brought back to its threshold amount and slash stayed the same Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -118,20 +122,20 @@ namespace Content.IntegrationTests.Tests.Destructible sThresholdListenerComponent.ThresholdsReached.Clear(); // Heal both types of damage to 0 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, -10, true)); - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Slash, -20, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, -20, true)); // No new thresholds reached, healing should not trigger it Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise blunt damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise slash damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Slash, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true)); // Both types of damage were healed and then raised again, the threshold should have been reached as triggers once is default false Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -157,20 +161,20 @@ namespace Content.IntegrationTests.Tests.Destructible threshold.TriggersOnce = true; // Heal blunt and slash back to 0 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, -10, true)); - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Slash, -10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, -10, true)); // No new thresholds reached from healing Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise blunt damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); // No new thresholds reached Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); // Raise slash damage to 10 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Slash, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true)); // No new thresholds reached as triggers once is set to true and it already triggered before Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs index ef93259d35..faf3fd1c87 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Threading.Tasks; using Content.Server.Destructible.Thresholds; using Content.Server.Destructible.Thresholds.Behaviors; @@ -8,6 +8,7 @@ using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; namespace Content.IntegrationTests.Tests.Destructible @@ -30,6 +31,7 @@ namespace Content.IntegrationTests.Tests.Destructible var sEntityManager = server.ResolveDependency(); var sMapManager = server.ResolveDependency(); + var sPrototypeManager = server.ResolveDependency(); IEntity sDestructibleEntity = null; IDamageableComponent sDamageableComponent = null; @@ -49,10 +51,11 @@ namespace Content.IntegrationTests.Tests.Destructible await server.WaitAssertion(() => { var coordinates = sDestructibleEntity.Transform.Coordinates; + var bruteDamageGroup = sPrototypeManager.Index("TestBrute"); Assert.DoesNotThrow(() => { - Assert.True(sDamageableComponent.ChangeDamage(DamageClass.Brute, 50, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 50, true)); }); Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs index 31e9021f50..5bb0244b88 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleTestPrototypes.cs @@ -6,9 +6,93 @@ namespace Content.IntegrationTests.Tests.Destructible public const string DestructibleEntityId = "DestructibleTestsDestructibleEntity"; public const string DestructibleDestructionEntityId = "DestructibleTestsDestructibleDestructionEntity"; public const string DestructibleDamageTypeEntityId = "DestructibleTestsDestructibleDamageTypeEntity"; - public const string DestructibleDamageClassEntityId = "DestructibleTestsDestructibleDamageClassEntity"; + public const string DestructibleDamageGroupEntityId = "DestructibleTestsDestructibleDamageGroupEntity"; public static readonly string Prototypes = $@" +- type: damageType + id: TestBlunt + +- type: damageType + id: TestSlash + +- type: damageType + id: TestPiercing + +- type: damageType + id: TestHeat + +- type: damageType + id: TestShock + +- type: damageType + id: TestCold + +- type: damageType + id: TestPoison + +- type: damageType + id: TestRadiation + +- type: damageType + id: TestAsphyxiation + +- type: damageType + id: TestBloodloss + +- type: damageType + id: TestCellular + +- type: damageGroup + id: TestBrute + damageTypes: + - TestBlunt + - TestSlash + - TestPiercing + +- type: damageGroup + id: TestBurn + damageTypes: + - TestHeat + - TestShock + - TestCold + +- type: damageGroup + id: TestAirloss + damageTypes: + - TestAsphyxiation + - TestBloodloss + +- type: damageGroup + id: TestToxin + damageTypes: + - TestPoison + - TestRadiation + +- type: damageGroup + id: TestGenetic + damageTypes: + - TestCellular + +- type: damageContainer + id: TestAllDamageContainer + supportAll: true + + +- type: damageContainer + id: TestBiologicalDamageContainer + supportedGroups: + - TestBrute + - TestBurn + - TestToxin + - TestAirloss + - TestGenetic + +- type: damageContainer + id: TestMetallicDamageContainer + supportedGroups: + - TestBrute + - TestBurn + - type: entity id: {SpawnedEntityId} name: {SpawnedEntityId} @@ -18,6 +102,7 @@ namespace Content.IntegrationTests.Tests.Destructible name: {DestructibleEntityId} components: - type: Damageable + damageContainer: TestMetallicDamageContainer - type: Destructible thresholds: - trigger: @@ -46,6 +131,7 @@ namespace Content.IntegrationTests.Tests.Destructible name: {DestructibleDestructionEntityId} components: - type: Damageable + damageContainer: TestMetallicDamageContainer - type: Destructible thresholds: - trigger: @@ -69,34 +155,36 @@ namespace Content.IntegrationTests.Tests.Destructible name: {DestructibleDamageTypeEntityId} components: - type: Damageable + damageContainer: TestMetallicDamageContainer - type: Destructible thresholds: - trigger: !type:AndTrigger triggers: - !type:DamageTypeTrigger - type: Blunt + damageType: TestBlunt damage: 10 - !type:DamageTypeTrigger - type: Slash + damageType: TestSlash damage: 10 - type: TestThresholdListener - type: entity - id: {DestructibleDamageClassEntityId} - name: {DestructibleDamageClassEntityId} + id: {DestructibleDamageGroupEntityId} + name: {DestructibleDamageGroupEntityId} components: - type: Damageable + damageContainer: TestMetallicDamageContainer - type: Destructible thresholds: - trigger: !type:AndTrigger triggers: - - !type:DamageClassTrigger - class: Brute + - !type:DamageGroupTrigger + damageGroup: TestBrute damage: 10 - - !type:DamageClassTrigger - class: Burn + - !type:DamageGroupTrigger + damageGroup: TestBurn damage: 10 - type: TestThresholdListener"; } diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs index d206ba6507..7f6db82e69 100644 --- a/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs +++ b/Content.IntegrationTests/Tests/Destructible/DestructibleThresholdActivationTest.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; +using Robust.Shared.Prototypes; using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes; namespace Content.IntegrationTests.Tests.Destructible @@ -35,6 +36,7 @@ namespace Content.IntegrationTests.Tests.Destructible var sEntityManager = server.ResolveDependency(); var sMapManager = server.ResolveDependency(); + var sPrototypeManager = server.ResolveDependency(); IEntity sDestructibleEntity; IDamageableComponent sDamageableComponent = null; @@ -62,12 +64,14 @@ namespace Content.IntegrationTests.Tests.Destructible await server.WaitAssertion(() => { - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true)); + var bluntDamageType = sPrototypeManager.Index("TestBlunt"); + + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); // No thresholds reached yet, the earliest one is at 20 damage Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); // Only one threshold reached, 20 Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -83,7 +87,7 @@ namespace Content.IntegrationTests.Tests.Destructible sThresholdListenerComponent.ThresholdsReached.Clear(); - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 30, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 30, true)); // One threshold reached, 50, since 20 already triggered before and it has not been healed below that amount Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -112,16 +116,16 @@ namespace Content.IntegrationTests.Tests.Destructible sThresholdListenerComponent.ThresholdsReached.Clear(); // Damage for 50 again, up to 100 now - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 50, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 50, true)); // No thresholds reached as they weren't healed below the trigger amount Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); - // Heal down to 0 - sDamageableComponent.Heal(); + // Set damage to 0 + sDamageableComponent.TrySetAllDamage(0); // Damage for 100, up to 100 - Assert.True(sDamageableComponent.ChangeDamage(DamageType.Blunt, 100, true)); + Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 100, true)); // Two thresholds reached as damage increased past the previous, 20 and 50 Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(2)); @@ -129,25 +133,25 @@ namespace Content.IntegrationTests.Tests.Destructible sThresholdListenerComponent.ThresholdsReached.Clear(); // Heal the entity for 40 damage, down to 60 - sDamageableComponent.ChangeDamage(DamageType.Blunt, -40, true); + sDamageableComponent.TryChangeDamage(bluntDamageType, -40, true); // Thresholds don't work backwards Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty); // Damage for 10, up to 70 - sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true); + sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true); // Not enough healing to de-trigger a threshold Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty); // Heal by 30, down to 40 - sDamageableComponent.ChangeDamage(DamageType.Blunt, -30, true); + sDamageableComponent.TryChangeDamage(bluntDamageType, -30, true); // Thresholds don't work backwards Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty); // Damage up to 50 again - sDamageableComponent.ChangeDamage(DamageType.Blunt, 10, true); + sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true); // The 50 threshold should have triggered again, after being healed Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); @@ -177,10 +181,10 @@ namespace Content.IntegrationTests.Tests.Destructible sThresholdListenerComponent.ThresholdsReached.Clear(); // Heal all damage - sDamageableComponent.Heal(); + sDamageableComponent.TrySetAllDamage(0); // Damage up to 50 - sDamageableComponent.ChangeDamage(DamageType.Blunt, 50, true); + sDamageableComponent.TryChangeDamage(bluntDamageType, 50, true); // Check that the total damage matches Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50)); @@ -228,7 +232,7 @@ namespace Content.IntegrationTests.Tests.Destructible sThresholdListenerComponent.ThresholdsReached.Clear(); // Heal the entity completely - sDamageableComponent.Heal(); + sDamageableComponent.TrySetAllDamage(0); // Check that the entity has 0 damage Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); @@ -241,7 +245,7 @@ namespace Content.IntegrationTests.Tests.Destructible } // Damage the entity up to 50 damage again - sDamageableComponent.ChangeDamage(DamageType.Blunt, 50, true); + sDamageableComponent.TryChangeDamage(bluntDamageType, 50, true); // Check that the total damage matches Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50)); diff --git a/Content.Server/Atmos/Components/BarotraumaComponent.cs b/Content.Server/Atmos/Components/BarotraumaComponent.cs index 06b98a835f..fc81f93d47 100644 --- a/Content.Server/Atmos/Components/BarotraumaComponent.cs +++ b/Content.Server/Atmos/Components/BarotraumaComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.CompilerServices; using Content.Server.Alert; using Content.Server.Pressure; @@ -7,6 +7,10 @@ using Content.Shared.Atmos; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; namespace Content.Server.Atmos.Components { @@ -18,6 +22,17 @@ namespace Content.Server.Atmos.Components { public override string Name => "Barotrauma"; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] private readonly string _damageTypeID = "Blunt"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update(float airPressure) { @@ -40,11 +55,11 @@ namespace Content.Server.Atmos.Components // Low pressure. case var p when p <= Atmospherics.WarningLowPressure: pressure *= lowPressureMultiplier; - - if(pressure > Atmospherics.WarningLowPressure) + if (pressure > Atmospherics.WarningLowPressure) goto default; - damageable.ChangeDamage(DamageType.Blunt, Atmospherics.LowPressureDamage, false, Owner); + // Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear. + damageable.TryChangeDamage(DamageType, Atmospherics.LowPressureDamage,true); if (status == null) break; @@ -66,7 +81,8 @@ namespace Content.Server.Atmos.Components var damage = (int) MathF.Min((pressure / Atmospherics.HazardHighPressure) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage); - damageable.ChangeDamage(DamageType.Blunt, damage, false, Owner); + // Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear. + damageable.TryChangeDamage(DamageType, damage,true); if (status == null) break; diff --git a/Content.Server/Atmos/Components/FlammableComponent.cs b/Content.Server/Atmos/Components/FlammableComponent.cs index 3cccaba238..86e1f3224b 100644 --- a/Content.Server/Atmos/Components/FlammableComponent.cs +++ b/Content.Server/Atmos/Components/FlammableComponent.cs @@ -20,6 +20,8 @@ using Robust.Shared.Localization; using Robust.Shared.Physics; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; namespace Content.Server.Atmos.Components { @@ -43,6 +45,18 @@ namespace Content.Server.Atmos.Components [DataField("canResistFire")] public bool CanResistFire { get; private set; } = false; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Heat"!; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + public void Extinguish() { if (!OnFire) return; @@ -92,7 +106,7 @@ namespace Content.Server.Atmos.Components { // TODO ATMOS Fire resistance from armor var damage = Math.Min((int) (FireStacks * 2.5f), 10); - damageable.ChangeDamage(DamageClass.Burn, damage, false); + damageable.TryChangeDamage(DamageType, damage, false); } AdjustFireStacks(-0.1f * (_resisting ? 10f : 1f)); diff --git a/Content.Server/Body/BodyManagerHealthChangeParams.cs b/Content.Server/Body/BodyManagerHealthChangeParams.cs index 7d50827125..5335b16772 100644 --- a/Content.Server/Body/BodyManagerHealthChangeParams.cs +++ b/Content.Server/Body/BodyManagerHealthChangeParams.cs @@ -1,4 +1,4 @@ -using Content.Shared.Body.Part; +using Content.Shared.Body.Part; using Content.Shared.Damage; namespace Content.Server.Body @@ -10,7 +10,7 @@ namespace Content.Server.Body } // TODO BODY: Remove and pretend it never existed - public class BodyDamageChangeParams : DamageChangeParams, IBodyHealthChangeParams + public class BodyDamageChangeParams : IBodyHealthChangeParams { public BodyDamageChangeParams(BodyPartType part) { diff --git a/Content.Server/Body/Respiratory/RespiratorComponent.cs b/Content.Server/Body/Respiratory/RespiratorComponent.cs index 2aec76e351..48f2472263 100644 --- a/Content.Server/Body/Respiratory/RespiratorComponent.cs +++ b/Content.Server/Body/Respiratory/RespiratorComponent.cs @@ -13,6 +13,8 @@ using Content.Shared.Atmos; using Content.Shared.Body.Components; using Content.Shared.Damage; using Content.Shared.Damage.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; using Content.Shared.MobState; using Content.Shared.Notification.Managers; using Robust.Shared.GameObjects; @@ -34,10 +36,6 @@ namespace Content.Server.Body.Respiratory private bool _isShivering; private bool _isSweating; - [ViewVariables(VVAccess.ReadWrite)] [DataField("suffocationDamage")] private int _suffocationDamage = 1; - - [ViewVariables(VVAccess.ReadWrite)] [DataField("suffocationDamageRecovery")] private int _suffocationDamageRecovery = 1; - [ViewVariables] [DataField("needsGases")] public Dictionary NeedsGases { get; set; } = new(); [ViewVariables] [DataField("producesGases")] public Dictionary ProducesGases { get; set; } = new(); @@ -94,6 +92,22 @@ namespace Content.Server.Body.Respiratory [ViewVariables] public bool Suffocating { get; private set; } + [ViewVariables(VVAccess.ReadWrite)] [DataField("suffocationDamage")] private int _damage = 1; + + [ViewVariables(VVAccess.ReadWrite)] [DataField("suffocationDamageRecovery")] private int _damageRecovery = 1; + + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Asphyxiation"!; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + private Dictionary NeedsAndDeficit(float frameTime) { var needs = new Dictionary(NeedsGases); @@ -349,7 +363,7 @@ namespace Content.Server.Body.Respiratory return; } - damageable.ChangeDamage(DamageType.Asphyxiation, _suffocationDamage, false); + damageable.TryChangeDamage(DamageType, _damage, false); } private void StopSuffocation() @@ -358,7 +372,7 @@ namespace Content.Server.Body.Respiratory if (Owner.TryGetComponent(out IDamageableComponent? damageable)) { - damageable.ChangeDamage(DamageType.Asphyxiation, -_suffocationDamageRecovery, false); + damageable.TryChangeDamage(DamageType, -_damageRecovery, false); } if (Owner.TryGetComponent(out ServerAlertsComponent? alertsComponent)) diff --git a/Content.Server/Chat/Commands/SuicideCommand.cs b/Content.Server/Chat/Commands/SuicideCommand.cs index 86686b6d99..6e9751f8d3 100644 --- a/Content.Server/Chat/Commands/SuicideCommand.cs +++ b/Content.Server/Chat/Commands/SuicideCommand.cs @@ -17,6 +17,7 @@ using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Prototypes; namespace Content.Server.Chat.Commands { @@ -34,21 +35,22 @@ namespace Content.Server.Chat.Commands var kind = suicide.Suicide(target, chat); if (kind != SuicideKind.Special) { - damageableComponent.SetDamage(kind switch + var prototypeManager = IoCManager.Resolve(); + damageableComponent.TrySetDamage(kind switch { - SuicideKind.Blunt => DamageType.Blunt, - SuicideKind.Slash => DamageType.Slash, - SuicideKind.Piercing => DamageType.Piercing, - SuicideKind.Heat => DamageType.Heat, - SuicideKind.Shock => DamageType.Shock, - SuicideKind.Cold => DamageType.Cold, - SuicideKind.Poison => DamageType.Poison, - SuicideKind.Radiation => DamageType.Radiation, - SuicideKind.Asphyxiation => DamageType.Asphyxiation, - SuicideKind.Bloodloss => DamageType.Bloodloss, - _ => DamageType.Blunt + SuicideKind.Blunt => prototypeManager.Index("Blunt"), + SuicideKind.Slash => prototypeManager.Index("Slash"), + SuicideKind.Piercing => prototypeManager.Index("Piercing"), + SuicideKind.Heat => prototypeManager.Index("Heat"), + SuicideKind.Shock => prototypeManager.Index("Shock"), + SuicideKind.Cold => prototypeManager.Index("Cold"), + SuicideKind.Poison => prototypeManager.Index("Poison"), + SuicideKind.Radiation => prototypeManager.Index("Radiation"), + SuicideKind.Asphyxiation => prototypeManager.Index("Asphyxiation"), + SuicideKind.Bloodloss => prototypeManager.Index("Bloodloss"), + _ => prototypeManager.Index("Blunt") }, - 200, source); + 200); } } @@ -117,7 +119,7 @@ namespace Content.Server.Chat.Commands var selfMessage = Loc.GetString("suicide-command-default-text-self"); owner.PopupMessage(selfMessage); - dmgComponent.SetDamage(DamageType.Piercing, 200, owner); + dmgComponent.TrySetDamage(IoCManager.Resolve().Index("Piercing"), 200); // Prevent the player from returning to the body. // Note that mind cannot be null because otherwise owner would be null. diff --git a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs b/Content.Server/Chemistry/ReagentEffects/HealthChange.cs index 47b640cdfc..7484f9cdde 100644 --- a/Content.Server/Chemistry/ReagentEffects/HealthChange.cs +++ b/Content.Server/Chemistry/ReagentEffects/HealthChange.cs @@ -5,6 +5,9 @@ using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; using Content.Shared.Damage; using Content.Shared.Damage.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; +using Robust.Shared.Serialization; namespace Content.Server.Chemistry.ReagentEffects { @@ -12,7 +15,7 @@ namespace Content.Server.Chemistry.ReagentEffects /// Default metabolism for medicine reagents. Attempts to find a DamageableComponent on the target, /// and to update its damage values. /// - public class HealthChange : ReagentEffect + public class HealthChange : ReagentEffect, ISerializationHooks { /// /// How much damage is changed when 1u of the reagent is metabolized. @@ -20,35 +23,45 @@ namespace Content.Server.Chemistry.ReagentEffects [DataField("healthChange")] public float AmountToChange { get; set; } = 1.0f; - /// - /// Class of damage changed, Brute, Burn, Toxin, Airloss. - /// - [DataField("damageClass")] - public DamageClass DamageType { get; set; } = DamageClass.Brute; + // TODO DAMAGE UNITS When damage units support decimals, get rid of this. + // See also _accumulatedDamage in ThirstComponent and HungerComponent + private float _accumulatedDamage; - private float _accumulatedHealth; + /// + /// Damage group to change. + /// + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove ISerializationHooks, if no longer needed. + [DataField("damageGroup", required: true)] + private readonly string _damageGroupID = default!; + public DamageGroupPrototype DamageGroup = default!; + void ISerializationHooks.AfterDeserialization() + { + DamageGroup = IoCManager.Resolve().Index(_damageGroupID); + } /// /// Changes damage if a DamageableComponent can be found. /// public override void Metabolize(IEntity solutionEntity, Solution.ReagentQuantity amount) { - if (solutionEntity.TryGetComponent(out IDamageableComponent? health)) + if (solutionEntity.TryGetComponent(out IDamageableComponent? damageComponent)) { - health.ChangeDamage(DamageType, (int)AmountToChange, true); - float decHealthChange = (float) (AmountToChange - (int) AmountToChange); - _accumulatedHealth += decHealthChange; + damageComponent.TryChangeDamage(DamageGroup, (int)AmountToChange, true); - if (_accumulatedHealth >= 1) + float decHealthChange = (float) (AmountToChange - (int) AmountToChange); + _accumulatedDamage += decHealthChange; + + if (_accumulatedDamage >= 1) { - health.ChangeDamage(DamageType, 1, true); - _accumulatedHealth -= 1; + damageComponent.TryChangeDamage(DamageGroup, 1, true); + _accumulatedDamage -= 1; } - else if(_accumulatedHealth <= -1) + else if(_accumulatedDamage <= -1) { - health.ChangeDamage(DamageType, -1, true); - _accumulatedHealth += 1; + damageComponent.TryChangeDamage(DamageGroup, -1, true); + _accumulatedDamage += 1; } } } diff --git a/Content.Server/Damage/Commands/HurtCommand.cs b/Content.Server/Damage/Commands/HurtCommand.cs index 0184c18c7e..851771a173 100644 --- a/Content.Server/Damage/Commands/HurtCommand.cs +++ b/Content.Server/Damage/Commands/HurtCommand.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Text; using Content.Server.Administration; using Content.Shared.Administration; @@ -9,6 +10,7 @@ using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Prototypes; namespace Content.Server.Damage.Commands { @@ -19,22 +21,24 @@ namespace Content.Server.Damage.Commands public string Description => "Ouch"; public string Help => $"Usage: {Command} () ()"; + private readonly IPrototypeManager _prototypeManager = default!; + public HurtCommand() { + _prototypeManager = IoCManager.Resolve(); + } + private string DamageTypes() { var msg = new StringBuilder(); - foreach (var dClass in Enum.GetNames(typeof(DamageClass))) + + foreach (var damageGroup in _prototypeManager.EnumeratePrototypes()) { - msg.Append($"\n{dClass}"); - - var types = Enum.Parse(dClass).ToTypes(); - - if (types.Count > 0) + msg.Append($"\n{damageGroup.ID}"); + if (damageGroup.DamageTypes.Any()) { msg.Append(": "); - msg.AppendJoin('|', types); + msg.AppendJoin('|', damageGroup.DamageTypes); } } - return $"Damage Types:{msg}"; } @@ -85,6 +89,8 @@ namespace Content.Server.Damage.Commands string[] args, [NotNullWhen(true)] out Damage? func) { + + if (!int.TryParse(args[1], out var amount)) { shell.WriteLine($"{args[1]} is not a valid damage integer."); @@ -93,42 +99,42 @@ namespace Content.Server.Damage.Commands return false; } - if (Enum.TryParse(args[0], true, out var damageClass)) + if (_prototypeManager.TryIndex(args[0], out var damageGroup)) { func = (damageable, ignoreResistances) => { - if (!damageable.DamageClasses.ContainsKey(damageClass)) + if (!damageable.ApplicableDamageGroups.Contains(damageGroup)) { - shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} can not be damaged with damage class {damageClass}"); + shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} can not be damaged with damage group {damageGroup}"); return; } - if (!damageable.ChangeDamage(damageClass, amount, ignoreResistances)) + if (!damageable.TryChangeDamage(damageGroup, amount, ignoreResistances)) { shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} received no damage."); return; } - shell.WriteLine($"Damaged entity {damageable.Owner.Name} with id {damageable.Owner.Uid} for {amount} {damageClass} damage{(ignoreResistances ? ", ignoring resistances." : ".")}"); + shell.WriteLine($"Damaged entity {damageable.Owner.Name} with id {damageable.Owner.Uid} for {amount} {damageGroup} damage{(ignoreResistances ? ", ignoring resistances." : ".")}"); }; return true; } // Fall back to DamageType - else if (Enum.TryParse(args[0], true, out var damageType)) + else if (_prototypeManager.TryIndex(args[0], out var damageType)) { func = (damageable, ignoreResistances) => { - if (!damageable.DamageTypes.ContainsKey(damageType)) + if (!damageable.IsSupportedDamageType(damageType)) { - shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} can not be damaged with damage class {damageType}"); + shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} can not be damaged with damage type {damageType}"); return; } - if (!damageable.ChangeDamage(damageType, amount, ignoreResistances)) + if (!damageable.TryChangeDamage(damageType, amount, ignoreResistances)) { shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} received no damage."); @@ -136,9 +142,10 @@ namespace Content.Server.Damage.Commands } shell.WriteLine($"Damaged entity {damageable.Owner.Name} with id {damageable.Owner.Uid} for {amount} {damageType} damage{(ignoreResistances ? ", ignoring resistances." : ".")}"); - }; + }; return true; + } else { diff --git a/Content.Server/Damage/Components/DamageOnHighSpeedImpactComponent.cs b/Content.Server/Damage/Components/DamageOnHighSpeedImpactComponent.cs index 6632f13c97..96553b9b7b 100644 --- a/Content.Server/Damage/Components/DamageOnHighSpeedImpactComponent.cs +++ b/Content.Server/Damage/Components/DamageOnHighSpeedImpactComponent.cs @@ -3,6 +3,9 @@ using Content.Shared.Damage; using Content.Shared.Sound; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.ViewVariables; namespace Content.Server.Damage.Components { @@ -14,8 +17,6 @@ namespace Content.Server.Damage.Components { public override string Name => "DamageOnHighSpeedImpact"; - [DataField("damage")] - public DamageType Damage { get; set; } = DamageType.Blunt; [DataField("minimumSpeed")] public float MinimumSpeed { get; set; } = 20f; [DataField("baseDamage")] @@ -34,5 +35,17 @@ namespace Content.Server.Damage.Components public float DamageCooldown { get; set; } = 2f; internal TimeSpan LastHit = TimeSpan.Zero; + + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } } } diff --git a/Content.Server/Damage/Components/DamageOnLandComponent.cs b/Content.Server/Damage/Components/DamageOnLandComponent.cs index e7a28fd05c..5beb3331f4 100644 --- a/Content.Server/Damage/Components/DamageOnLandComponent.cs +++ b/Content.Server/Damage/Components/DamageOnLandComponent.cs @@ -3,6 +3,9 @@ using Content.Shared.Damage.Components; using Content.Shared.Throwing; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; namespace Content.Server.Damage.Components { @@ -11,20 +14,31 @@ namespace Content.Server.Damage.Components { public override string Name => "DamageOnLand"; - [DataField("damageType")] - private DamageType _damageType = DamageType.Blunt; - [DataField("amount")] + [ViewVariables(VVAccess.ReadWrite)] private int _amount = 1; [DataField("ignoreResistances")] + [ViewVariables(VVAccess.ReadWrite)] private bool _ignoreResistances; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + void ILand.Land(LandEventArgs eventArgs) { - if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) return; - - damageable.ChangeDamage(_damageType, _amount, _ignoreResistances, eventArgs.User); + if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) + return; + damageable.TryChangeDamage(DamageType, _amount, _ignoreResistances); } } } diff --git a/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs b/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs index 54ef9c45c9..061ff309a7 100644 --- a/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs +++ b/Content.Server/Damage/Components/DamageOnToolInteractComponent.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Content.Server.Tools.Components; using Content.Shared.Damage; @@ -7,12 +7,16 @@ using Content.Shared.Interaction; using Content.Shared.Tool; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; namespace Content.Server.Damage.Components { [RegisterComponent] public class DamageOnToolInteractComponent : Component, IInteractUsing { + public override string Name => "DamageOnToolInteract"; [DataField("damage")] @@ -21,6 +25,23 @@ namespace Content.Server.Damage.Components [DataField("tools")] private List _tools = new(); + // TODO PROTOTYPE Replace these datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("weldingDamageType")] + private readonly string _weldingDamageTypeID = "Heat"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype WeldingDamageType = default!; + [DataField("defaultDamageType")] + private readonly string _defaultDamageTypeID = "Blunt"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DefaultDamageType = default!; + protected override void Initialize() + { + base.Initialize(); + WeldingDamageType = IoCManager.Resolve().Index(_weldingDamageTypeID); + DefaultDamageType = IoCManager.Resolve().Index(_defaultDamageTypeID); + } + async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) { if (eventArgs.Using.TryGetComponent(out var tool)) @@ -44,17 +65,15 @@ namespace Content.Server.Damage.Components protected bool CallDamage(InteractUsingEventArgs eventArgs, ToolComponent tool) { - if (eventArgs.Target.TryGetComponent(out var damageable)) - { - damageable.ChangeDamage(tool.HasQuality(ToolQuality.Welding) - ? DamageType.Heat - : DamageType.Blunt, - Damage, false, eventArgs.User); + if (!eventArgs.Target.TryGetComponent(out var damageable)) + return false; - return true; - } + damageable.TryChangeDamage(tool.HasQuality(ToolQuality.Welding) + ? WeldingDamageType + : DefaultDamageType, + Damage); - return false; + return true; } } } diff --git a/Content.Server/Damage/Components/DamageOtherOnHitComponent.cs b/Content.Server/Damage/Components/DamageOtherOnHitComponent.cs index f0e008f6f1..ec04d76358 100644 --- a/Content.Server/Damage/Components/DamageOtherOnHitComponent.cs +++ b/Content.Server/Damage/Components/DamageOtherOnHitComponent.cs @@ -2,6 +2,8 @@ using Content.Shared.Damage; using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; namespace Content.Server.Damage.Components { @@ -11,13 +13,21 @@ namespace Content.Server.Damage.Components { public override string Name => "DamageOtherOnHit"; - [DataField("damageType")] - public DamageType DamageType { get; } = DamageType.Blunt; - [DataField("amount")] public int Amount { get; } = 1; [DataField("ignoreResistances")] public bool IgnoreResistances { get; } = false; + + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"; + public DamageTypePrototype DamageType { get; set; } = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } } } diff --git a/Content.Server/Damage/DamageOnHighSpeedImpactSystem.cs b/Content.Server/Damage/DamageOnHighSpeedImpactSystem.cs index 9b11b23dd1..3d2dd3b301 100644 --- a/Content.Server/Damage/DamageOnHighSpeedImpactSystem.cs +++ b/Content.Server/Damage/DamageOnHighSpeedImpactSystem.cs @@ -46,7 +46,7 @@ namespace Content.Server.Damage if (ComponentManager.TryGetComponent(uid, out StunnableComponent? stun) && _robustRandom.Prob(component.StunChance)) stun.Stun(component.StunSeconds); - damageable.ChangeDamage(component.Damage, damage, false, args.OtherFixture.Body.Owner); + damageable.TryChangeDamage(component.DamageType, damage); } } } diff --git a/Content.Server/Damage/DamageOtherOnHitSystem.cs b/Content.Server/Damage/DamageOtherOnHitSystem.cs index e70be630c1..27323162d9 100644 --- a/Content.Server/Damage/DamageOtherOnHitSystem.cs +++ b/Content.Server/Damage/DamageOtherOnHitSystem.cs @@ -17,7 +17,7 @@ namespace Content.Server.Damage if (!args.Target.TryGetComponent(out IDamageableComponent? damageable)) return; - damageable.ChangeDamage(component.DamageType, component.Amount, component.IgnoreResistances, args.User); + damageable.TryChangeDamage(component.DamageType, component.Amount, component.IgnoreResistances); } } } diff --git a/Content.Server/Damage/GodmodeSystem.cs b/Content.Server/Damage/GodmodeSystem.cs index 823c155fae..f80aaa28ce 100644 --- a/Content.Server/Damage/GodmodeSystem.cs +++ b/Content.Server/Damage/GodmodeSystem.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using Content.Server.Atmos.Components; +using System.Collections.Generic; using System.Linq; +using Content.Server.Atmos.Components; using Content.Shared.Damage; using Content.Shared.Damage.Components; -using Content.Shared.Damage.Resistances; using Content.Shared.GameTicking; using JetBrains.Annotations; using Robust.Shared.GameObjects; @@ -43,8 +42,9 @@ namespace Content.Server.Damage if (entity.TryGetComponent(out IDamageableComponent? damageable)) { - damageable.SupportedTypes.Clear(); - damageable.SupportedClasses.Clear(); + damageable.SupportedDamageTypes.Clear(); + damageable.FullySupportedDamageGroups.Clear(); + damageable.ApplicableDamageGroups.Clear(); } return true; @@ -69,14 +69,19 @@ namespace Content.Server.Damage if (entity.TryGetComponent(out IDamageableComponent? damageable)) { - if (old.SupportedTypes != null) + if (old.SupportedDamageTypes != null) { - damageable.SupportedTypes.UnionWith(old.SupportedTypes); + damageable.SupportedDamageTypes.UnionWith(old.SupportedDamageTypes); } - if (old.SupportedClasses != null) + if (old.SupportedDamageGroups != null) { - damageable.SupportedClasses.UnionWith(old.SupportedClasses); + damageable.FullySupportedDamageGroups.UnionWith(old.SupportedDamageGroups); + } + + if (old.ApplicableDamageGroups != null) + { + damageable.ApplicableDamageGroups.UnionWith(old.ApplicableDamageGroups); } } @@ -111,8 +116,9 @@ namespace Content.Server.Damage if (entity.TryGetComponent(out IDamageableComponent? damageable)) { - SupportedTypes = damageable.SupportedTypes.ToHashSet(); - SupportedClasses = damageable.SupportedClasses.ToHashSet(); + SupportedDamageTypes = damageable.SupportedDamageTypes.ToHashSet(); + SupportedDamageGroups = damageable.FullySupportedDamageGroups.ToHashSet(); + ApplicableDamageGroups = damageable.ApplicableDamageGroups.ToHashSet(); } } @@ -120,9 +126,11 @@ namespace Content.Server.Damage public bool MovedByPressure { get; } - public HashSet? SupportedTypes { get; } + public HashSet? SupportedDamageTypes { get; } - public HashSet? SupportedClasses { get; } + public HashSet? SupportedDamageGroups { get; } + + public HashSet? ApplicableDamageGroups { get; } } } } diff --git a/Content.Server/Damage/RejuvenateVerb.cs b/Content.Server/Damage/RejuvenateVerb.cs index 9447d2dfd3..5a5ef2bff7 100644 --- a/Content.Server/Damage/RejuvenateVerb.cs +++ b/Content.Server/Damage/RejuvenateVerb.cs @@ -61,7 +61,7 @@ namespace Content.Server.Damage { if (target.TryGetComponent(out IDamageableComponent? damage)) { - damage.Heal(); + damage.TrySetAllDamage(0); } if (target.TryGetComponent(out IMobStateComponent? mobState)) diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageClassTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageClassTrigger.cs deleted file mode 100644 index 606f99cc3b..0000000000 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageClassTrigger.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Content.Shared.Damage; -using Content.Shared.Damage.Components; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Server.Destructible.Thresholds.Triggers -{ - /// - /// A trigger that will activate when the amount of damage received - /// of the specified class is above the specified threshold. - /// - [Serializable] - [DataDefinition] - public class DamageClassTrigger : IThresholdTrigger - { - /// - /// The class to check the damage of. - /// - [DataField("class")] - public DamageClass? Class { get; set; } - - /// - /// The amount of damage at which this threshold will trigger. - /// - [DataField("damage")] - public int Damage { get; set; } - - public bool Reached(IDamageableComponent damageable, DestructibleSystem system) - { - if (Class == null) - { - return false; - } - - return damageable.TryGetDamage(Class.Value, out var damageReceived) && - damageReceived >= Damage; - } - } -} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs new file mode 100644 index 0000000000..ccddc1678f --- /dev/null +++ b/Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs @@ -0,0 +1,42 @@ +using System; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; + +namespace Content.Server.Destructible.Thresholds.Triggers +{ + /// + /// A trigger that will activate when the amount of damage received + /// of the specified class is above the specified threshold. + /// + [Serializable] + [DataDefinition] + public class DamageGroupTrigger : IThresholdTrigger + { + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // While you're at it, maybe also combine damageGroup and damage into a dictionary, and allow it to test a sum + // of damage types? + [DataField("damageGroup", required: true)] + private string _damageGroupID { get; set; } = default!; + public DamageGroupPrototype DamageGroup => IoCManager.Resolve().Index(_damageGroupID); + + /// + /// The amount of damage at which this threshold will trigger. + /// + [DataField("damage", required: true)] + public int Damage { get; set; } = default!; + + public bool Reached(IDamageableComponent damageable, DestructibleSystem system) + { + if (DamageGroup == null) + { + return false; + } + + return damageable.TryGetDamage(DamageGroup, out var damageReceived) && + damageReceived >= Damage; + } + } +} diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs index a0ea9d10ea..db51dd217f 100644 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs +++ b/Content.Server/Destructible/Thresholds/Triggers/DamageTrigger.cs @@ -1,4 +1,4 @@ -using System; +using System; using Content.Shared.Damage.Components; using Robust.Shared.Serialization.Manager.Attributes; @@ -15,8 +15,8 @@ namespace Content.Server.Destructible.Thresholds.Triggers /// /// The amount of damage at which this threshold will trigger. /// - [DataField("damage")] - public int Damage { get; set; } + [DataField("damage", required: true)] + public int Damage { get; set; } = default!; public bool Reached(IDamageableComponent damageable, DestructibleSystem system) { diff --git a/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs b/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs index 80451c0ae7..7dbfb7ad87 100644 --- a/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs +++ b/Content.Server/Destructible/Thresholds/Triggers/DamageTypeTrigger.cs @@ -1,7 +1,9 @@ -using System; +using System; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; namespace Content.Server.Destructible.Thresholds.Triggers { @@ -13,20 +15,24 @@ namespace Content.Server.Destructible.Thresholds.Triggers [DataDefinition] public class DamageTypeTrigger : IThresholdTrigger { - [DataField("type")] - public DamageType? Type { get; set; } + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // While you're at it, maybe also combine damageGroup and damage into a dictionary, and allow it to test a sum + // of damage types? + [DataField("damageType", required:true)] + public string _damageTypeID { get; set; } = default!; + public DamageTypePrototype DamageType => IoCManager.Resolve().Index(_damageTypeID); - [DataField("damage")] - public int Damage { get; set; } + [DataField("damage", required: true)] + public int Damage { get; set; } = default!; public bool Reached(IDamageableComponent damageable, DestructibleSystem system) { - if (Type == null) + if (DamageType == null) { return false; } - return damageable.TryGetDamage(Type.Value, out var damageReceived) && + return damageable.TryGetDamage(DamageType, out var damageReceived) && damageReceived >= Damage; } } diff --git a/Content.Server/Doors/Components/ServerDoorComponent.cs b/Content.Server/Doors/Components/ServerDoorComponent.cs index 85908e86a0..7c14d435bb 100644 --- a/Content.Server/Doors/Components/ServerDoorComponent.cs +++ b/Content.Server/Doors/Components/ServerDoorComponent.cs @@ -30,6 +30,8 @@ using Robust.Shared.Players; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; using Timer = Robust.Shared.Timing.Timer; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; namespace Content.Server.Doors.Components { @@ -45,6 +47,18 @@ namespace Content.Server.Doors.Components [DataField("tryOpenDoorSound")] private SoundSpecifier _tryOpenDoorSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg"); + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + public override DoorState State { get => base.State; @@ -536,7 +550,7 @@ namespace Content.Server.Doors.Components hitsomebody = true; CurrentlyCrushing.Add(e.Owner.Uid); - damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner); + damage.TryChangeDamage(DamageType, DoorCrushDamage); stun.Paralyze(DoorStunTime); } diff --git a/Content.Server/GameTicking/Presets/GamePreset.cs b/Content.Server/GameTicking/Presets/GamePreset.cs index 922ce47c02..4138737f0f 100644 --- a/Content.Server/GameTicking/Presets/GamePreset.cs +++ b/Content.Server/GameTicking/Presets/GamePreset.cs @@ -10,6 +10,7 @@ using Robust.Server.Player; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Network; +using Robust.Shared.Prototypes; namespace Content.Server.GameTicking.Presets { @@ -66,7 +67,8 @@ namespace Content.Server.GameTicking.Presets if (playerEntity.TryGetComponent(out IDamageableComponent? damageable)) { //todo: what if they dont breathe lol - damageable.SetDamage(DamageType.Asphyxiation, 200, playerEntity); + //cry deeply + damageable.TrySetDamage(IoCManager.Resolve().Index("Asphyxiation"), 200); } } } diff --git a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs index 6886a01401..7ab697642a 100644 --- a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs +++ b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs @@ -23,6 +23,7 @@ using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Prototypes; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Map; @@ -38,6 +39,7 @@ namespace Content.Server.GameTicking.Presets [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; public string PDAPrototypeName => "CaptainPDA"; public string BeltPrototypeName => "ClothingBeltJanitorFilled"; @@ -192,11 +194,11 @@ namespace Content.Server.GameTicking.Presets { if (mobState.IsCritical()) { - // TODO: This is copy/pasted from ghost code. Really, IDamagableComponent needs a method to reliably kill the target. + // TODO: This is copy/pasted from ghost code. Really, IDamageableComponent needs a method to reliably kill the target. if (entity.TryGetComponent(out IDamageableComponent? damageable)) { //todo: what if they dont breathe lol - damageable.ChangeDamage(DamageType.Asphyxiation, 100, true); + damageable.TryChangeDamage(_prototypeManager.Index("Asphyxiation"), 100, true); } } else if (!mobState.IsDead()) diff --git a/Content.Server/Light/Components/PoweredLightComponent.cs b/Content.Server/Light/Components/PoweredLightComponent.cs index 5f38eda681..f666e251ec 100644 --- a/Content.Server/Light/Components/PoweredLightComponent.cs +++ b/Content.Server/Light/Components/PoweredLightComponent.cs @@ -20,6 +20,7 @@ using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Prototypes; using Robust.Shared.Localization; using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; @@ -77,6 +78,19 @@ namespace Content.Server.Light.Components [ViewVariables] private ContainerSlot _lightBulbContainer = default!; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + [DataField("damageType")] + private readonly string _damageTypeID = "Heat"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + _lightBulbContainer = ContainerHelpers.EnsureContainer(Owner, "light_bulb"); + } + [ViewVariables] public LightBulbComponent? LightBulb { @@ -126,7 +140,7 @@ namespace Content.Server.Light.Components void Burn() { Owner.PopupMessage(eventArgs.User, Loc.GetString("powered-light-component-burn-hand")); - damageableComponent.ChangeDamage(DamageType.Heat, 20, false, Owner); + damageableComponent.TryChangeDamage(DamageType, 20); SoundSystem.Play(Filter.Pvs(Owner), _burnHandSound.GetSound(), Owner); } @@ -249,13 +263,6 @@ namespace Content.Server.Light.Components } } - protected override void Initialize() - { - base.Initialize(); - - _lightBulbContainer = ContainerHelpers.EnsureContainer(Owner, "light_bulb"); - } - public override void HandleMessage(ComponentMessage message, IComponent? component) { base.HandleMessage(message, component); diff --git a/Content.Server/Medical/Components/HealingComponent.cs b/Content.Server/Medical/Components/HealingComponent.cs index 2d523bec28..990a26b438 100644 --- a/Content.Server/Medical/Components/HealingComponent.cs +++ b/Content.Server/Medical/Components/HealingComponent.cs @@ -1,15 +1,17 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using Content.Server.Stack; using Content.Shared.ActionBlocker; using Content.Shared.Damage; using Content.Shared.Damage.Components; using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Helpers; using Content.Shared.Stacks; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; namespace Content.Server.Medical.Components { @@ -18,7 +20,12 @@ namespace Content.Server.Medical.Components { public override string Name => "Healing"; - [DataField("heal")] public Dictionary Heal { get; private set; } = new(); + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // This also requires changing the dictionary type, and removing a _prototypeManager.Index() call. + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [DataField("heal", required: true )] + [ViewVariables(VVAccess.ReadWrite)] + public Dictionary Heal = new(); async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { @@ -48,9 +55,9 @@ namespace Content.Server.Medical.Components return true; } - foreach (var (type, amount) in Heal) + foreach (var (damageTypeID, amount) in Heal) { - damageable.ChangeDamage(type, -amount, true); + damageable.TryChangeDamage(_prototypeManager.Index(damageTypeID), -amount, true); } return true; diff --git a/Content.Server/Medical/Components/MedicalScannerComponent.cs b/Content.Server/Medical/Components/MedicalScannerComponent.cs index 3df1cc4d6d..c1a6aff647 100644 --- a/Content.Server/Medical/Components/MedicalScannerComponent.cs +++ b/Content.Server/Medical/Components/MedicalScannerComponent.cs @@ -99,8 +99,8 @@ namespace Content.Server.Medical.Components private static readonly MedicalScannerBoundUserInterfaceState EmptyUIState = new( null, - new Dictionary(), - new Dictionary(), + new Dictionary(), + new Dictionary(), false); private MedicalScannerBoundUserInterfaceState GetUserInterfaceState() @@ -121,12 +121,13 @@ namespace Content.Server.Medical.Components return EmptyUIState; } - var classes = new Dictionary(damageable.DamageClasses); - var types = new Dictionary(damageable.DamageTypes); + // Get dictionaries of damage, by fully supported damage groups and types + var groups = new Dictionary(damageable.GetDamagePerFullySupportedGroupIDs); + var types = new Dictionary(damageable.GetDamagePerTypeIDs); if (_bodyContainer.ContainedEntity?.Uid == null) { - return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, true); + return new MedicalScannerBoundUserInterfaceState(body.Uid, groups, types, true); } var cloningSystem = EntitySystem.Get(); @@ -134,7 +135,7 @@ namespace Content.Server.Medical.Components mindComponent.Mind != null && cloningSystem.HasDnaScan(mindComponent.Mind); - return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, scanned); + return new MedicalScannerBoundUserInterfaceState(body.Uid, groups, types, scanned); } private void UpdateUserInterface() diff --git a/Content.Server/Mining/Components/AsteroidRockComponent.cs b/Content.Server/Mining/Components/AsteroidRockComponent.cs index 4de41b4275..a01af77651 100644 --- a/Content.Server/Mining/Components/AsteroidRockComponent.cs +++ b/Content.Server/Mining/Components/AsteroidRockComponent.cs @@ -7,9 +7,12 @@ using Content.Shared.Mining; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.IoC; using Robust.Shared.Player; using Robust.Shared.Random; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; namespace Content.Server.Mining.Components { @@ -21,10 +24,16 @@ namespace Content.Server.Mining.Components public override string Name => "AsteroidRock"; private static readonly string[] SpriteStates = {"0", "1", "2", "3", "4"}; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"!; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() { base.Initialize(); - + DamageType = IoCManager.Resolve().Index(_damageTypeID); if (Owner.TryGetComponent(out AppearanceComponent? appearance)) { appearance.SetData(AsteroidRockVisuals.State, _random.Pick(SpriteStates)); @@ -37,7 +46,7 @@ namespace Content.Server.Mining.Components if (!item.TryGetComponent(out MeleeWeaponComponent? meleeWeaponComponent)) return false; - Owner.GetComponent().ChangeDamage(DamageType.Blunt, meleeWeaponComponent.Damage, false, item); + Owner.GetComponent().TryChangeDamage(DamageType, meleeWeaponComponent.Damage); if (!item.TryGetComponent(out PickaxeComponent? pickaxeComponent)) return true; diff --git a/Content.Server/Nutrition/Components/HungerComponent.cs b/Content.Server/Nutrition/Components/HungerComponent.cs index c53047bd45..3aae6b5bee 100644 --- a/Content.Server/Nutrition/Components/HungerComponent.cs +++ b/Content.Server/Nutrition/Components/HungerComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Content.Server.Alert; using Content.Shared.Alert; @@ -14,6 +14,7 @@ using Robust.Shared.Players; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; +using Robust.Shared.Prototypes; namespace Content.Server.Nutrition.Components { @@ -22,6 +23,10 @@ namespace Content.Server.Nutrition.Components { [Dependency] private readonly IRobustRandom _random = default!; + // TODO DAMAGE UNITS When damage units support decimals, get rid of this. + // See also _accumulatedDamage in ThirstComponent and HealthChange. + private float _accumulatedDamage; + // Base stuff [ViewVariables(VVAccess.ReadWrite)] public float BaseDecayRate @@ -29,7 +34,7 @@ namespace Content.Server.Nutrition.Components get => _baseDecayRate; set => _baseDecayRate = value; } - [DataField("base_decay_rate")] + [DataField("baseDecayRate")] private float _baseDecayRate = 0.1f; [ViewVariables(VVAccess.ReadWrite)] @@ -59,11 +64,11 @@ namespace Content.Server.Nutrition.Components public Dictionary HungerThresholds => _hungerThresholds; private readonly Dictionary _hungerThresholds = new() { - {HungerThreshold.Overfed, 600.0f}, - {HungerThreshold.Okay, 450.0f}, - {HungerThreshold.Peckish, 300.0f}, - {HungerThreshold.Starving, 150.0f}, - {HungerThreshold.Dead, 0.0f}, + { HungerThreshold.Overfed, 600.0f }, + { HungerThreshold.Okay, 450.0f }, + { HungerThreshold.Peckish, 300.0f }, + { HungerThreshold.Starving, 150.0f }, + { HungerThreshold.Dead, 0.0f }, }; public static readonly Dictionary HungerThresholdAlertTypes = new() @@ -73,6 +78,18 @@ namespace Content.Server.Nutrition.Components { HungerThreshold.Starving, AlertType.Starving }, }; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"!; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + public void HungerThresholdEffect(bool force = false) { if (_currentHungerThreshold != _lastHungerThreshold || force) @@ -177,6 +194,7 @@ namespace Content.Server.Nutrition.Components if (_currentHungerThreshold != HungerThreshold.Dead) return; + // --> Current Hunger is below dead threshold if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) return; @@ -186,7 +204,14 @@ namespace Content.Server.Nutrition.Components if (!mobState.IsDead()) { - damageable.ChangeDamage(DamageType.Blunt, 2, true); + // --> But they are not dead yet. + var damage = 2 * frametime; + _accumulatedDamage += damage - ((int) damage); + damageable.TryChangeDamage(DamageType, (int) damage); + if (_accumulatedDamage >= 1) { + _accumulatedDamage -= 1; + damageable.TryChangeDamage(DamageType, 1, true); + } } } diff --git a/Content.Server/Nutrition/Components/ThirstComponent.cs b/Content.Server/Nutrition/Components/ThirstComponent.cs index 0717a0849f..3826e398b2 100644 --- a/Content.Server/Nutrition/Components/ThirstComponent.cs +++ b/Content.Server/Nutrition/Components/ThirstComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Content.Server.Alert; using Content.Shared.Alert; @@ -14,6 +14,7 @@ using Robust.Shared.Players; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; +using Robust.Shared.Prototypes; namespace Content.Server.Nutrition.Components { @@ -22,6 +23,10 @@ namespace Content.Server.Nutrition.Components { [Dependency] private readonly IRobustRandom _random = default!; + // TODO DAMAGE UNITS When damage units support decimals, get rid of this. + // See also _accumulatedDamage in HungerComponent and HealthChange. + private float _accumulatedDamage; + // Base stuff [ViewVariables(VVAccess.ReadWrite)] public float BaseDecayRate @@ -29,7 +34,7 @@ namespace Content.Server.Nutrition.Components get => _baseDecayRate; set => _baseDecayRate = value; } - [DataField("base_decay_rate")] + [DataField("baseDecayRate")] private float _baseDecayRate = 0.1f; [ViewVariables(VVAccess.ReadWrite)] @@ -72,6 +77,18 @@ namespace Content.Server.Nutrition.Components {ThirstThreshold.Parched, AlertType.Parched}, }; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + public void ThirstThresholdEffect(bool force = false) { if (_currentThirstThreshold != _lastThirstThreshold || force) @@ -174,6 +191,7 @@ namespace Content.Server.Nutrition.Components if (_currentThirstThreshold != ThirstThreshold.Dead) return; + // --> Current Hunger is below dead threshold if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) return; @@ -183,7 +201,15 @@ namespace Content.Server.Nutrition.Components if (!mobState.IsDead()) { - damageable.ChangeDamage(DamageType.Blunt, 2, true); + // --> But they are not dead yet. + var damage = 2 * frametime; + _accumulatedDamage += damage - ((int) damage); + damageable.TryChangeDamage(DamageType, (int) damage); + if (_accumulatedDamage >= 1) + { + _accumulatedDamage -= 1; + damageable.TryChangeDamage(DamageType, 1, true); + } } } diff --git a/Content.Server/Nutrition/EntitySystems/HungerSystem.cs b/Content.Server/Nutrition/EntitySystems/HungerSystem.cs index b2bf8eaee9..1234f6ac75 100644 --- a/Content.Server/Nutrition/EntitySystems/HungerSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/HungerSystem.cs @@ -19,7 +19,7 @@ namespace Content.Server.Nutrition.EntitySystems { comp.OnUpdate(_accumulatedFrameTime); } - _accumulatedFrameTime -= 1; + _accumulatedFrameTime = 0; } } } diff --git a/Content.Server/Projectiles/Components/HitscanComponent.cs b/Content.Server/Projectiles/Components/HitscanComponent.cs index 721aac12ab..2274774edf 100644 --- a/Content.Server/Projectiles/Components/HitscanComponent.cs +++ b/Content.Server/Projectiles/Components/HitscanComponent.cs @@ -9,8 +9,10 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; +using Robust.Shared.ViewVariables; namespace Content.Server.Projectiles.Components { @@ -25,26 +27,18 @@ namespace Content.Server.Projectiles.Components public override string Name => "Hitscan"; public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask; + [DataField("layers")] //todo WithFormat.Flags() private int _collisionMask = (int) CollisionGroup.Opaque; - - public float Damage - { - get => _damage; - set => _damage = value; - } [DataField("damage")] - private float _damage = 10f; - public DamageType DamageType => _damageType; - [DataField("damageType")] - private DamageType _damageType = DamageType.Heat; - public float MaxLength => 20.0f; + public float Damage { get; set; } = 10f; + public float MaxLength => 20.0f; private TimeSpan _startTime; private TimeSpan _deathTime; public float ColorModifier { get; set; } = 1.0f; - [DataField("spriteName")] + [DataField("spriteName")] private string _spriteName = "Objects/Weapons/Guns/Projectiles/laser.png"; [DataField("muzzleFlash")] private string? _muzzleFlash; @@ -53,6 +47,19 @@ namespace Content.Server.Projectiles.Components [DataField("soundHitWall")] private SoundSpecifier _soundHitWall = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg"); + + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Piercing"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } + public void FireEffects(IEntity user, float distance, Angle angle, IEntity? hitEntity = null) { var effectSystem = EntitySystem.Get(); diff --git a/Content.Server/Projectiles/Components/ProjectileComponent.cs b/Content.Server/Projectiles/Components/ProjectileComponent.cs index d307be7458..36e7297fef 100644 --- a/Content.Server/Projectiles/Components/ProjectileComponent.cs +++ b/Content.Server/Projectiles/Components/ProjectileComponent.cs @@ -13,14 +13,12 @@ namespace Content.Server.Projectiles.Components [ComponentReference(typeof(SharedProjectileComponent))] public class ProjectileComponent : SharedProjectileComponent { - [DataField("damages")] private Dictionary _damages = new(); - - [ViewVariables] - public Dictionary Damages - { - get => _damages; - set => _damages = value; - } + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // This also requires changing the dictionary type and modifying ProjectileSystem.cs, which uses it. + // While thats being done, also replace "damages" -> "damageTypes" For consistency. + [DataField("damages")] + [ViewVariables(VVAccess.ReadWrite)] + public Dictionary Damages { get; set; } = new(); [DataField("deleteOnCollide")] public bool DeleteOnCollide { get; } = true; diff --git a/Content.Server/Projectiles/ProjectileSystem.cs b/Content.Server/Projectiles/ProjectileSystem.cs index 4894e81878..d6783440af 100644 --- a/Content.Server/Projectiles/ProjectileSystem.cs +++ b/Content.Server/Projectiles/ProjectileSystem.cs @@ -7,12 +7,17 @@ using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.IoC; +using Content.Shared.Damage; namespace Content.Server.Projectiles { [UsedImplicitly] internal sealed class ProjectileSystem : EntitySystem { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + public override void Initialize() { base.Initialize(); @@ -49,9 +54,9 @@ namespace Content.Server.Projectiles { EntityManager.TryGetEntity(component.Shooter, out var shooter); - foreach (var (damageType, amount) in component.Damages) + foreach (var (damageTypeID, amount) in component.Damages) { - damage.ChangeDamage(damageType, amount, false, shooter); + damage.TryChangeDamage(_prototypeManager.Index(damageTypeID), amount); } component.DamagedEntity = true; diff --git a/Content.Server/Repairable/RepairableComponent.cs b/Content.Server/Repairable/RepairableComponent.cs index 8c618957d0..8d5e99ad2f 100644 --- a/Content.Server/Repairable/RepairableComponent.cs +++ b/Content.Server/Repairable/RepairableComponent.cs @@ -36,7 +36,7 @@ namespace Content.Server.Repairable { if (!await welder.UseTool(eventArgs.User, Owner, _doAfterDelay, ToolQuality.Welding, _fuelCost)) return false; - damageable.Heal(); + damageable.TrySetAllDamage(0); Owner.PopupMessage(eventArgs.User, Loc.GetString("comp-repairable-repair", diff --git a/Content.Server/Spawners/Components/SpawnPointComponent.cs b/Content.Server/Spawners/Components/SpawnPointComponent.cs index 6165ba0b33..2369bbcf64 100644 --- a/Content.Server/Spawners/Components/SpawnPointComponent.cs +++ b/Content.Server/Spawners/Components/SpawnPointComponent.cs @@ -18,7 +18,7 @@ namespace Content.Server.Spawners.Components [DataField("job_id")] private string? _jobId; - [field: ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadWrite)] [DataField("spawn_type")] public SpawnPointType SpawnType { get; } = SpawnPointType.Unset; diff --git a/Content.Server/Temperature/Components/TemperatureComponent.cs b/Content.Server/Temperature/Components/TemperatureComponent.cs index e988ac4462..360c2666c6 100644 --- a/Content.Server/Temperature/Components/TemperatureComponent.cs +++ b/Content.Server/Temperature/Components/TemperatureComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using Content.Server.Alert; using Content.Shared.Alert; using Content.Shared.Atmos; @@ -8,6 +8,8 @@ using Robust.Shared.GameObjects; using Robust.Shared.Physics; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; namespace Content.Server.Temperature.Components { @@ -22,11 +24,21 @@ namespace Content.Server.Temperature.Components /// public override string Name => "Temperature"; - [ViewVariables] public float CurrentTemperature { get => _currentTemperature; set => _currentTemperature = value; } + [DataField("heatDamageThreshold")] + private float _heatDamageThreshold = default; + [DataField("coldDamageThreshold")] + private float _coldDamageThreshold = default; + [DataField("tempDamageCoefficient")] + private float _tempDamageCoefficient = 1; + [DataField("currentTemperature")] + public float CurrentTemperature { get; set; } = Atmospherics.T20C; + [DataField("specificHeat")] + private float _specificHeat = Atmospherics.MinimumHeatCapacity; [ViewVariables] public float HeatDamageThreshold => _heatDamageThreshold; [ViewVariables] public float ColdDamageThreshold => _coldDamageThreshold; [ViewVariables] public float TempDamageCoefficient => _tempDamageCoefficient; + [ViewVariables] public float SpecificHeat => _specificHeat; [ViewVariables] public float HeatCapacity { get { @@ -39,33 +51,25 @@ namespace Content.Server.Temperature.Components } } - [ViewVariables] public float SpecificHeat => _specificHeat; - - [DataField("heatDamageThreshold")] - private float _heatDamageThreshold = default; - [DataField("coldDamageThreshold")] - private float _coldDamageThreshold = default; - [DataField("tempDamageCoefficient")] - private float _tempDamageCoefficient = 1; - [DataField("currentTemperature")] - private float _currentTemperature = Atmospherics.T20C; - [DataField("specificHeat")] - private float _specificHeat = Atmospherics.MinimumHeatCapacity; + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("coldDamageType")] + private readonly string _coldDamageTypeID = "Cold"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype ColdDamageType = default!; + [DataField("hotDamageType")] + private readonly string _hotDamageTypeID = "Heat"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype HotDamageType = default!; + protected override void Initialize() + { + base.Initialize(); + ColdDamageType = IoCManager.Resolve().Index(_coldDamageTypeID); + HotDamageType = IoCManager.Resolve().Index(_hotDamageTypeID); + } public void Update() { - var tempDamage = 0; - DamageType? damageType = null; - if (CurrentTemperature >= _heatDamageThreshold) - { - tempDamage = (int) Math.Floor((CurrentTemperature - _heatDamageThreshold) * _tempDamageCoefficient); - damageType = DamageType.Heat; - } - else if (CurrentTemperature <= _coldDamageThreshold) - { - tempDamage = (int) Math.Floor((_coldDamageThreshold - CurrentTemperature) * _tempDamageCoefficient); - damageType = DamageType.Cold; - } if (Owner.TryGetComponent(out ServerAlertsComponent? status)) { @@ -108,10 +112,19 @@ namespace Content.Server.Temperature.Components } } - if (!damageType.HasValue) return; - if (!Owner.TryGetComponent(out IDamageableComponent? component)) return; - component.ChangeDamage(damageType.Value, tempDamage, false); + + if (CurrentTemperature >= _heatDamageThreshold) + { + int tempDamage = (int) Math.Floor((CurrentTemperature - _heatDamageThreshold) * _tempDamageCoefficient); + component.TryChangeDamage(HotDamageType, tempDamage, false); + } + else if (CurrentTemperature <= _coldDamageThreshold) + { + int tempDamage = (int) Math.Floor((_coldDamageThreshold - CurrentTemperature) * _tempDamageCoefficient); + component.TryChangeDamage(ColdDamageType, tempDamage, false); + } + } /// diff --git a/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs b/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs index 23c2a45a3d..92cddcb470 100644 --- a/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs +++ b/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs @@ -4,6 +4,8 @@ using Content.Shared.Sound; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; namespace Content.Server.Weapon.Melee.Components { @@ -48,15 +50,23 @@ namespace Content.Server.Weapon.Melee.Components [DataField("damage")] public int Damage { get; set; } = 5; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("damageType")] - public DamageType DamageType { get; set; } = DamageType.Blunt; - [ViewVariables(VVAccess.ReadWrite)] [DataField("clickAttackEffect")] public bool ClickAttackEffect { get; set; } = true; public TimeSpan LastAttackTime; public TimeSpan CooldownEnd; + + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // Also remove Initialize override, if no longer needed. + [DataField("damageType")] + private readonly string _damageTypeID = "Blunt"; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype DamageType = default!; + protected override void Initialize() + { + base.Initialize(); + DamageType = IoCManager.Resolve().Index(_damageTypeID); + } } } diff --git a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs index 73b26b878a..e0d3315252 100644 --- a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs @@ -25,6 +25,8 @@ namespace Content.Server.Weapon.Melee public sealed class MeleeWeaponSystem : EntitySystem { [Dependency] private IGameTiming _gameTiming = default!; + + public override void Initialize() { base.Initialize(); @@ -88,7 +90,7 @@ namespace Content.Server.Weapon.Melee if (target.TryGetComponent(out IDamageableComponent? damageableComponent)) { - damageableComponent.ChangeDamage(comp.DamageType, comp.Damage, false, owner); + damageableComponent.TryChangeDamage(comp.DamageType, comp.Damage); } SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), target); @@ -157,7 +159,7 @@ namespace Content.Server.Weapon.Melee { if (entity.TryGetComponent(out var damageComponent)) { - damageComponent.ChangeDamage(comp.DamageType, comp.Damage, false, owner); + damageComponent.TryChangeDamage(comp.DamageType, comp.Damage); } } } diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs index bc47b11bb9..a406693da7 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/ServerBatteryBarrelComponent.cs @@ -188,7 +188,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components { if (energyRatio < 1.0) { - var newDamages = new Dictionary(projectileComponent.Damages.Count); + var newDamages = new Dictionary(projectileComponent.Damages.Count); foreach (var (damageType, damage) in projectileComponent.Damages) { newDamages.Add(damageType, (int) (damage * energyRatio)); diff --git a/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs b/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs index c647af9292..790a21497d 100644 --- a/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs +++ b/Content.Server/Weapon/Ranged/Barrels/Components/ServerRangedBarrelComponent.cs @@ -399,7 +399,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components if (!result.HitEntity.TryGetComponent(out IDamageableComponent? damageable)) return; - damageable.ChangeDamage(hitscan.DamageType, (int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero), false, Owner); + damageable.TryChangeDamage(hitscan.DamageType, (int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero)); //I used Math.Round over Convert.toInt32, as toInt32 always rounds to //even numbers if halfway between two numbers, rather than rounding to nearest } diff --git a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs b/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs index 8a6fdeaccf..ef890b15a7 100644 --- a/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs +++ b/Content.Server/Weapon/Ranged/ServerRangedWeaponComponent.cs @@ -27,6 +27,8 @@ using Robust.Shared.Players; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; using Robust.Shared.ViewVariables; +using Robust.Shared.Prototypes; +using System.Collections.Generic; namespace Content.Server.Weapon.Ranged { @@ -55,6 +57,17 @@ namespace Content.Server.Weapon.Ranged [DataField("clumsyWeaponShotSound")] private SoundSpecifier _clumsyWeaponShotSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"); + + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + // This also requires changing the dictionary type and modifying TryFire(), which uses it. + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [ViewVariables(VVAccess.ReadWrite)] + [DataField("clumsyDamage")] + public Dictionary ClumsyDamage { get; set; } = new() + { + { "Blunt", 10 }, + { "Heat", 5 } + }; public Func? WeaponCanFireHandler; public Func? UserCanFireHandler; @@ -168,25 +181,30 @@ namespace Content.Server.Weapon.Ranged if (ClumsyCheck && ClumsyComponent.TryRollClumsy(user, ClumsyExplodeChance)) { - SoundSystem.Play( + //Wound them + if (user.TryGetComponent(out IDamageableComponent? health)) + { + foreach (KeyValuePair damage in ClumsyDamage) + { + health.TryChangeDamage(_prototypeManager.Index(damage.Key), damage.Value); + } + } + + // Knock them down + if (user.TryGetComponent(out StunnableComponent? stun)) + { + stun.Paralyze(3f); + } + + // Apply salt to the wound ("Honk!") + SoundSystem.Play( Filter.Pvs(Owner), _clumsyWeaponHandlingSound.GetSound(), Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5)); SoundSystem.Play( Filter.Pvs(Owner), _clumsyWeaponShotSound.GetSound(), Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5)); - - if (user.TryGetComponent(out IDamageableComponent? health)) - { - health.ChangeDamage(DamageType.Blunt, 10, false, user); - health.ChangeDamage(DamageType.Heat, 5, false, user); - } - - if (user.TryGetComponent(out StunnableComponent? stun)) - { - stun.Paralyze(3f); - } - + user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy")); Owner.Delete(); diff --git a/Content.Shared/Body/Components/SharedBodyComponent.cs b/Content.Shared/Body/Components/SharedBodyComponent.cs index 909fe9f930..883cc771f4 100644 --- a/Content.Shared/Body/Components/SharedBodyComponent.cs +++ b/Content.Shared/Body/Components/SharedBodyComponent.cs @@ -73,6 +73,23 @@ namespace Content.Shared.Body.Components public SharedBodyPartComponent? CenterPart => CenterSlot?.Part; + /// + /// Amount of damage to deal when all vital organs are removed. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("vitalPartsRemovedDamage")] + public int VitalPartsRemovedDamage { get; set; } = 300!; + + /// + /// Damage type to deal when all vital organs are removed. + /// + // TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. + [ViewVariables] + [DataField("vitalPartsRemovedDamageType")] + private string _vitalPartsRemovedDamageTypeID { get; set; } = "Bloodloss"!; + [ViewVariables(VVAccess.ReadWrite)] + public DamageTypePrototype VitalPartsRemovedDamageType = default!; + protected override void Initialize() { base.Initialize(); @@ -81,6 +98,7 @@ namespace Content.Shared.Body.Components // TODO BODY Move to template or somewhere else if (TemplateId != null) { + VitalPartsRemovedDamageType = _prototypeManager.Index(_vitalPartsRemovedDamageTypeID); var template = _prototypeManager.Index(TemplateId); foreach (var (id, partType) in template.Slots) @@ -194,7 +212,7 @@ namespace Content.Shared.Body.Components { if (part.IsVital && SlotParts.Count(x => x.Value.PartType == part.PartType) == 0) { - damageable.ChangeDamage(DamageType.Bloodloss, 300, true); // TODO BODY KILL + damageable.TryChangeDamage(VitalPartsRemovedDamageType, VitalPartsRemovedDamage, true); // TODO BODY KILL } } diff --git a/Content.Shared/Damage/Components/DamageableComponent.cs b/Content.Shared/Damage/Components/DamageableComponent.cs index 71df457e1e..a547f2731f 100644 --- a/Content.Shared/Damage/Components/DamageableComponent.cs +++ b/Content.Shared/Damage/Components/DamageableComponent.cs @@ -18,8 +18,15 @@ namespace Content.Shared.Damage.Components { /// /// Component that allows attached entities to take damage. - /// This basic version never dies (thus can take an indefinite amount of damage). /// + /// + /// The supported damage types are specified using a 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). + /// [RegisterComponent] [ComponentReference(typeof(IDamageableComponent))] [NetworkedComponent()] @@ -27,58 +34,76 @@ namespace Content.Shared.Damage.Components { public override string Name => "Damageable"; - // TODO define these in yaml? - public const string DefaultResistanceSet = "defaultResistances"; - public const string DefaultDamageContainer = "metallicDamageContainer"; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - private readonly Dictionary _damageList = DamageTypeExtensions.ToNewDictionary(); + /// + /// The main damage dictionary. All the damage information is stored in this dictionary with keys. + /// + private Dictionary _damageDict = new(); - [DataField("resistances")] public string ResistanceSetId = DefaultResistanceSet; - - // 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; } = DefaultDamageContainer; + [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 - [ViewVariables] public int TotalDamage => _damageList.Values.Sum(); + // When moving logic from damageableComponent --> Damage System, make damageSystem update these on damage change. + [ViewVariables] public int TotalDamage => _damageDict.Values.Sum(); + [ViewVariables] public IReadOnlyDictionary GetDamagePerType => _damageDict; + [ViewVariables] public IReadOnlyDictionary GetDamagePerApplicableGroup => DamageTypeDictToDamageGroupDict(_damageDict, ApplicableDamageGroups); + [ViewVariables] public IReadOnlyDictionary GetDamagePerFullySupportedGroup => DamageTypeDictToDamageGroupDict(_damageDict, FullySupportedDamageGroups); - [ViewVariables] public IReadOnlyDictionary DamageClasses => _damageList.ToClassDictionary(); + // Whenever sending over network, also need a dictionary + // TODO DAMAGE MAYBE Cache this? + public IReadOnlyDictionary GetDamagePerApplicableGroupIDs => ConvertDictKeysToIDs(GetDamagePerApplicableGroup); + public IReadOnlyDictionary GetDamagePerFullySupportedGroupIDs => ConvertDictKeysToIDs(GetDamagePerFullySupportedGroup); + public IReadOnlyDictionary GetDamagePerTypeIDs => ConvertDictKeysToIDs(_damageDict); - [ViewVariables] public IReadOnlyDictionary DamageTypes => _damageList; + // 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 RadiationDamageTypeIDs { get; set; } = new() {"Radiation"}; + [ViewVariables] + [DataField("explosionDamageTypes")] + public List ExplosionDamageTypeIDs { get; set; } = new() { "Piercing", "Heat" }; - [ViewVariables] public HashSet SupportedTypes { get; } = new(); + public HashSet ApplicableDamageGroups { get; } = new(); - [ViewVariables] public HashSet SupportedClasses { get; } = new(); + public HashSet FullySupportedDamageGroups { get; } = new(); - public bool SupportsDamageClass(DamageClass @class) - { - return SupportedClasses.Contains(@class); - } - - public bool SupportsDamageType(DamageType type) - { - return SupportedTypes.Contains(type); - } + public HashSet SupportedDamageTypes { get; } = new(); protected override void Initialize() { base.Initialize(); - var prototypeManager = IoCManager.Resolve(); - // TODO DAMAGE Serialize damage done and resistance changes - var damagePrototype = prototypeManager.Index(DamageContainerId); + var damageContainerPrototype = _prototypeManager.Index(DamageContainerId); - SupportedClasses.Clear(); - SupportedTypes.Clear(); + ApplicableDamageGroups.Clear(); + FullySupportedDamageGroups.Clear(); + SupportedDamageTypes.Clear(); - DamageContainerId = damagePrototype.ID; - SupportedClasses.UnionWith(damagePrototype.SupportedClasses); - SupportedTypes.UnionWith(damagePrototype.SupportedTypes); + //Get Damage groups/types from the DamageContainerPrototype. + DamageContainerId = damageContainerPrototype.ID; + ApplicableDamageGroups.UnionWith(damageContainerPrototype.ApplicableDamageGroups); + FullySupportedDamageGroups.UnionWith(damageContainerPrototype.FullySupportedDamageGroups); + SupportedDamageTypes.UnionWith(damageContainerPrototype.SupportedDamageTypes); - var resistancePrototype = prototypeManager.Index(ResistanceSetId); - Resistances = new ResistanceSet(resistancePrototype); + //initialize damage dictionary 0 damage + _damageDict = new(SupportedDamageTypes.Count); + foreach (var type in SupportedDamageTypes) + { + _damageDict.Add(type, 0); + } + + Resistances = new ResistanceSet(_prototypeManager.Index(ResistanceSetId)); } protected override void Startup() @@ -90,7 +115,7 @@ namespace Content.Shared.Damage.Components public override ComponentState GetComponentState(ICommonSession player) { - return new DamageableComponentState(_damageList); + return new DamageableComponentState(GetDamagePerTypeIDs); } public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) @@ -102,113 +127,101 @@ namespace Content.Shared.Damage.Components return; } - _damageList.Clear(); + _damageDict.Clear(); - foreach (var (type, damage) in state.DamageList) + foreach (var (type, damage) in state.DamageDict) { - _damageList[type] = damage; + _damageDict[_prototypeManager.Index(type)] = damage; } } - public int GetDamage(DamageType type) + public int GetDamage(DamageTypePrototype type) { - return _damageList.GetValueOrDefault(type); + return GetDamagePerType.GetValueOrDefault(type); } - public bool TryGetDamage(DamageType type, out int damage) + public bool TryGetDamage(DamageTypePrototype type, out int damage) { - return _damageList.TryGetValue(type, out damage); + return GetDamagePerType.TryGetValue(type, out damage); } - public int GetDamage(DamageClass @class) + public int GetDamage(DamageGroupPrototype group) { - if (!SupportsDamageClass(@class)) - { - return 0; - } - - var damage = 0; - - foreach (var type in @class.ToTypes()) - { - damage += GetDamage(type); - } - - return damage; + return GetDamagePerApplicableGroup.GetValueOrDefault(group); } - public bool TryGetDamage(DamageClass @class, out int damage) + public bool TryGetDamage(DamageGroupPrototype group, out int damage) { - if (!SupportsDamageClass(@class)) + 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)) { - damage = 0; return false; } - damage = GetDamage(@class); + if (newValue < 0) + { + // invalid value + return false; + } + + foreach (var type in group.DamageTypes) + { + TrySetDamage(type, newValue); + } return true; } - /// - /// Attempts to set the damage value for the given . - /// - /// - /// True if successful, false if this container does not support that type. - /// - public bool TrySetDamage(DamageType type, int newValue) + public bool TrySetAllDamage(int newValue) { if (newValue < 0) { + // invalid value return false; } - var damageClass = type.ToClass(); - - if (SupportedClasses.Contains(damageClass)) + foreach (var type in SupportedDamageTypes) { - var old = _damageList[type] = newValue; - _damageList[type] = newValue; - - var delta = newValue - old; - var datum = new DamageChangeData(type, newValue, delta); - var data = new List {datum}; - - OnHealthChanged(data); - - return true; + TrySetDamage(type, newValue); } - return false; + return true; } - public void Heal(DamageType type) + public bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false) { - SetDamage(type, 0); - } - - public void Heal() - { - foreach (var type in SupportedTypes) - { - Heal(type); - } - } - - public bool ChangeDamage( - DamageType type, - int amount, - bool ignoreResistances, - IEntity? source = null, - DamageChangeParams? extraParams = null) - { - if (!SupportsDamageType(type)) + // 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 (!ignoreResistances) + if (!ignoreDamageResistances) { finalDamage = Resistances.CalculateDamage(type, amount); } @@ -216,24 +229,23 @@ namespace Content.Shared.Damage.Components if (finalDamage == 0) return false; - if (!_damageList.TryGetValue(type, out var current)) - { - 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; - _damageList[type] = 0; + + // Cap healing down to zero + _damageDict[type] = 0; finalDamage = -current; } else { - _damageList[type] = current + finalDamage; + _damageDict[type] = current + finalDamage; } - current = _damageList[type]; + current = _damageDict[type]; var datum = new DamageChangeData(type, current, finalDamage); var data = new List {datum}; @@ -243,107 +255,117 @@ namespace Content.Shared.Damage.Components return true; } - public bool ChangeDamage(DamageClass @class, int amount, bool ignoreResistances, - IEntity? source = null, - DamageChangeParams? extraParams = null) + public bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false) { - if (!SupportsDamageClass(@class)) - { - return false; - } - - var types = @class.ToTypes(); + var types = group.DamageTypes.ToArray(); if (amount < 0) { - // Changing multiple types is a bit more complicated. Might be a better way (formula?) to do this, - // but essentially just loops between each damage category until all healing is used up. - var healingLeft = -amount; - var healThisCycle = 1; + // We are Healing. Keep track of how much we can hand out (with a better var name for readability). + var availableHealing = -amount; - // While we have healing left... - while (healingLeft > 0 && healThisCycle != 0) + // 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) { - // Infinite loop fallback, if no healing was done in a cycle - // then exit - healThisCycle = 0; - - int healPerType; - if (healingLeft < types.Count) - { - // Say we were to distribute 2 healing between 3 - // this will distribute 1 to each (and stop after 2 are given) - healPerType = 1; - } - else - { - // Say we were to distribute 62 healing between 3 - // this will distribute 20 to each, leaving 2 for next loop - healPerType = healingLeft / types.Count; - } - - foreach (var type in types) - { - var damage = GetDamage(type); - var healAmount = Math.Min(healingLeft, damage); - healAmount = Math.Min(healAmount, healPerType); - - ChangeDamage(type, -healAmount, true); - healThisCycle += healAmount; - healingLeft -= healAmount; - } - } - - return true; - } - - var damageLeft = amount; - - while (damageLeft > 0) - { - int damagePerType; - - if (damageLeft < types.Count) - { - damagePerType = 1; - } - else - { - damagePerType = damageLeft / types.Count; + TrySetDamage(group, 0); + return true; } + // Partially heal each damage group + int healing, damage; foreach (var type in types) { - var damageAmount = Math.Min(damagePerType, damageLeft); - ChangeDamage(type, damageAmount, true); - damageLeft -= damageAmount; + 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; } - return true; + // amount==0 no damage change. + return false; } - public bool SetDamage(DamageType type, int newValue, IEntity? source = null, DamageChangeParams? extraParams = null) + public bool TrySetDamage(DamageTypePrototype type, int newValue) { - if (newValue >= TotalDamage) + if (!_damageDict.TryGetValue(type, out var oldValue)) { return false; } if (newValue < 0) { + // invalid value return false; } - if (!_damageList.ContainsKey(type)) + if (oldValue == newValue) { - return false; + // No health change. + // But we are trying to set, not trying to change. + return true; } - var old = _damageList[type]; - _damageList[type] = newValue; + _damageDict[type] = newValue; - var delta = newValue - old; + var delta = newValue - oldValue; var datum = new DamageChangeData(type, 0, delta); var data = new List {datum}; @@ -356,7 +378,7 @@ namespace Content.Shared.Damage.Components { var data = new List(); - foreach (var type in SupportedTypes) + foreach (var type in SupportedDamageTypes) { var damage = GetDamage(type); var datum = new DamageChangeData(type, damage, 0); @@ -382,11 +404,15 @@ namespace Content.Shared.Damage.Components Dirty(); } - void IRadiationAct.RadiationAct(float frameTime, SharedRadiationPulseComponent radiation) + public void RadiationAct(float frameTime, SharedRadiationPulseComponent radiation) { var totalDamage = Math.Max((int)(frameTime * radiation.RadsPerSecond), 1); - ChangeDamage(DamageType.Radiation, totalDamage, false, radiation.Owner); + foreach (var typeID in RadiationDamageTypeIDs) + { + TryChangeDamage(_prototypeManager.Index(typeID), totalDamage); + } + } public void OnExplosion(ExplosionEventArgs eventArgs) @@ -399,19 +425,75 @@ namespace Content.Shared.Damage.Components _ => throw new ArgumentOutOfRangeException() }; - ChangeDamage(DamageType.Piercing, damage, false); - ChangeDamage(DamageType.Heat, damage, false); + foreach (var typeID in ExplosionDamageTypeIDs) + { + TryChangeDamage(_prototypeManager.Index(typeID), damage); + } } + + /// + /// Take a dictionary with keys and return a dictionary using as keys + /// instead. + /// + /// + /// Useful when sending damage type and group prototypes dictionaries over the network. + /// + public static IReadOnlyDictionary + ConvertDictKeysToIDs(IReadOnlyDictionary prototypeDict) + where TPrototype : IPrototype + { + Dictionary idDict = new(prototypeDict.Count); + foreach (var entry in prototypeDict) + { + idDict.Add(entry.Key.ID, entry.Value); + } + return idDict; + } + + /// + /// Convert a dictionary with damage type keys to a dictionary of damage groups keys. + /// + /// + /// 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. + /// + public static IReadOnlyDictionary + DamageTypeDictToDamageGroupDict(IReadOnlyDictionary damageTypeDict, IEnumerable groupKeys) + { + var damageGroupDict = new Dictionary(); + 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 Dictionary DamageList; + public readonly IReadOnlyDictionary DamageDict; + + public DamageableComponentState(IReadOnlyDictionary damageDict) - public DamageableComponentState(Dictionary damageList) { - DamageList = damageList; + DamageDict = damageDict; } } } diff --git a/Content.Shared/Damage/Components/IDamageableComponent.cs b/Content.Shared/Damage/Components/IDamageableComponent.cs index e00bc605a8..54fbece0dd 100644 --- a/Content.Shared/Damage/Components/IDamageableComponent.cs +++ b/Content.Shared/Damage/Components/IDamageableComponent.cs @@ -8,137 +8,214 @@ namespace Content.Shared.Damage.Components public interface IDamageableComponent : IComponent, IExAct { /// - /// Sum of all damages taken. + /// The sum of all damages types in the DamageableComponent. /// int TotalDamage { get; } /// - /// The amount of damage mapped by . + /// Returns a dictionary of the damage in the container, indexed by applicable . /// - IReadOnlyDictionary DamageClasses { get; } + /// + /// 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 + /// + IReadOnlyDictionary GetDamagePerApplicableGroup { get; } /// - /// The amount of damage mapped by . + /// Returns a dictionary of the damage in the container, indexed by fully supported instances of . /// - IReadOnlyDictionary DamageTypes { get; } + /// + /// 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 . 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. + /// + IReadOnlyDictionary GetDamagePerFullySupportedGroup { get; } - HashSet SupportedTypes { get; } + /// + /// Returns a dictionary of the damage in the container, indexed by . + /// + IReadOnlyDictionary GetDamagePerType { get; } - HashSet SupportedClasses { get; } + /// + /// Like , but indexed by + /// + IReadOnlyDictionary GetDamagePerApplicableGroupIDs { get; } + + /// + /// Like , but indexed by + /// + IReadOnlyDictionary GetDamagePerFullySupportedGroupIDs { get; } + + /// + /// Like , but indexed by + /// + IReadOnlyDictionary GetDamagePerTypeIDs { get; } + + /// + /// Collection of damage types supported by this DamageableComponent. + /// + /// + /// 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 + /// + HashSet SupportedDamageTypes { get; } + + /// + /// Collection of damage groups that are fully supported by DamageableComponent. + /// + /// + /// 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 , and to see all applicable damage groups . + /// + HashSet FullySupportedDamageGroups { get; } + + /// + /// Collection of damage groups that could apply damage to this DamageableComponent. + /// + /// + /// 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 . + /// + HashSet ApplicableDamageGroups { get; } /// /// The resistances of this component. /// ResistanceSet Resistances { get; } - bool SupportsDamageClass(DamageClass @class); - - bool SupportsDamageType(DamageType type); - /// - /// Gets the amount of damage of a type. + /// Tries to get the amount of damage of a type. /// /// The type to get the damage of. /// The amount of damage of that type. /// /// True if the given is supported, false otherwise. /// - bool TryGetDamage(DamageType type, out int damage); + bool TryGetDamage(DamageTypePrototype type, out int damage); /// - /// Gets the amount of damage of a class. + /// Returns the amount of damage of a given type, or zero if it is not supported. /// - /// The class to get the damage of. - /// The amount of damage of that class. - /// - /// True if the given is supported, false otherwise. - /// - bool TryGetDamage(DamageClass @class, out int damage); + int GetDamage(DamageTypePrototype type); /// - /// Changes the specified , applying - /// resistance values only if it is damage. + /// Tries to get the total amount of damage in a damage group. + /// + /// The group to get the damage of. + /// The amount of damage in that group. + /// + /// True if the given group is applicable to this container, false otherwise. + /// + bool TryGetDamage(DamageGroupPrototype group, out int damage); + + /// + /// Returns the amount of damage present in an applicable group, or zero if no members are supported. + /// + int GetDamage(DamageGroupPrototype group); + + /// + /// Tries to change the specified , applying + /// resistance values only if it is dealing damage. /// /// Type of damage being changed. /// /// Amount of damage being received (positive for damage, negative for heals). /// - /// - /// Whether or not to ignore resistances. + /// + /// Whether or not to ignore resistances when taking damage. /// Healing always ignores resistances, regardless of this input. /// - /// - /// The entity that dealt or healed the damage, if any. - /// - /// - /// Extra parameters that some components may require, such as a specific limb to target. - /// /// - /// False if the given type is not supported or improper - /// were provided; true otherwise. + /// False if the given type is not supported or no damage change occurred; true otherwise. /// - bool ChangeDamage( - DamageType type, - int amount, - bool ignoreResistances, - IEntity? source = null, - DamageChangeParams? extraParams = null); + bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false); /// - /// Changes the specified , applying - /// resistance values only if it is damage. - /// Spreads amount evenly between the s - /// represented by that class. + /// Tries to change damage of the specified , applying resistance values + /// only if it is damage. /// - /// Class of damage being changed. + /// + /// + /// If dealing damage, this spreads the damage change amount evenly between the 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). + /// + /// + /// If healing damage, this spreads the damage change proportional to the current damage value of each (subject to integer rounding). If there is less damage than is being + /// healed, some healing is wasted. Unsupported damage types do not waste healing. + /// + /// + /// group of damage being changed. /// /// Amount of damage being received (positive for damage, negative for heals). /// - /// - /// Whether to ignore resistances. - /// Healing always ignores resistances, regardless of this input. - /// - /// Entity that dealt or healed the damage, if any. - /// - /// Extra parameters that some components may require, - /// such as a specific limb to target. + /// + /// Whether to ignore resistances when taking damage. Healing always ignores resistances, regardless of this + /// input. /// /// - /// Returns false if the given class is not supported or improper - /// were provided; true otherwise. + /// Returns false if the given group is not applicable or no damage change occurred; true otherwise. /// - bool ChangeDamage( - DamageClass @class, - int amount, - bool ignoreResistances, - IEntity? source = null, - DamageChangeParams? extraParams = null); + bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false); /// - /// Forcefully sets the specified to the given - /// value, ignoring resistance values. + /// Forcefully sets the specified to the given value, ignoring resistance + /// values. /// - /// Type of damage being changed. + /// Type of damage being set. /// New damage value to be set. - /// Entity that set the new damage value. - /// - /// Extra parameters that some components may require, - /// such as a specific limb to target. - /// /// - /// Returns false if the given type is not supported or improper - /// were provided; true otherwise. + /// Returns false if a given type is not supported or a negative value is provided; true otherwise. /// - bool SetDamage( - DamageType type, - int newValue, - IEntity? source = null, - DamageChangeParams? extraParams = null); + bool TrySetDamage(DamageTypePrototype type, int newValue); /// - /// Sets all damage values to zero. + /// Forcefully sets all damage types in a specified damage group using . /// - void Heal(); + /// + /// 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. + /// + /// Group of damage being set. + /// New damage value to be set. + /// + /// Returns false if the given group is not applicable or a negative value is provided; true otherwise. + /// + bool TrySetDamage(DamageGroupPrototype group, int newValue); + + /// + /// Sets all supported damage types to specified value using . + /// + /// New damage value to be set. + /// + /// Returns false if a negative value is provided; true otherwise. + /// + bool TrySetAllDamage(int newValue); + + /// + /// Returns true if the given damage group is applicable to this damage container. + /// + public bool IsApplicableDamageGroup(DamageGroupPrototype group); + + /// + /// Returns true if the given damage group is fully supported by this damage container. + /// + public bool IsFullySupportedDamageGroup(DamageGroupPrototype group); + + /// + /// Returns true if the given damage type is supported by this damage container. + /// + public bool IsSupportedDamageType(DamageTypePrototype type); + /// /// Invokes the HealthChangedEvent with the current values of health. diff --git a/Content.Shared/Damage/Container/DamageContainerPrototype.cs b/Content.Shared/Damage/Container/DamageContainerPrototype.cs index 37d0b3afea..51abd75224 100644 --- a/Content.Shared/Damage/Container/DamageContainerPrototype.cs +++ b/Content.Shared/Damage/Container/DamageContainerPrototype.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; @@ -8,45 +9,142 @@ using Robust.Shared.ViewVariables; namespace Content.Shared.Damage.Container { /// - /// Prototype for the DamageContainer class. + /// A damage container which can be used to specify support for various damage types. /// + /// + /// 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 should support. + /// [Prototype("damageContainer")] [Serializable, NetSerializable] public class DamageContainerPrototype : IPrototype, ISerializationHooks { - [DataField("supportAll")] private bool _supportAll; - [DataField("supportedClasses")] private HashSet _supportedClasses = new(); - [DataField("supportedTypes")] private HashSet _supportedTypes = new(); - - // TODO NET 5 IReadOnlySet - [ViewVariables] public IReadOnlyCollection SupportedClasses => _supportedClasses; - - [ViewVariables] public IReadOnlyCollection SupportedTypes => _supportedTypes; + private IPrototypeManager _prototypeManager = default!; [ViewVariables] [DataField("id", required: true)] public string ID { get; } = default!; + /// + /// Determines whether this DamageContainerPrototype will support ALL damage types and groups. If true, + /// ignore all other options. + /// + [DataField("supportAll")] private bool _supportAll; + + [DataField("supportedGroups")] private HashSet _supportedDamageGroupIDs = new(); + [DataField("supportedTypes")] private HashSet _supportedDamageTypeIDs = new(); + + private HashSet _applicableDamageGroups = new(); + private HashSet _fullySupportedDamageGroups = new(); + private HashSet _supportedDamageTypes = new(); + + // TODO NET 5 IReadOnlySet + + /// + /// Collection of damage groups that can affect this container. + /// + /// + /// 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 . + /// + [ViewVariables] public IReadOnlyCollection ApplicableDamageGroups => _applicableDamageGroups; + + /// + /// Collection of damage groups that are fully supported by this container. + /// + /// + /// 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 , and to see all applicable damage groups . + /// + [ViewVariables] public IReadOnlyCollection FullySupportedDamageGroups => _fullySupportedDamageGroups; + + /// + /// Collection of damage types supported by this container. + /// + /// + /// 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 + /// + [ViewVariables] public IReadOnlyCollection SupportedDamageTypes => _supportedDamageTypes; + void ISerializationHooks.AfterDeserialization() { + _prototypeManager = IoCManager.Resolve(); + if (_supportAll) { - _supportedClasses.UnionWith(Enum.GetValues()); - _supportedTypes.UnionWith(Enum.GetValues()); + foreach (var group in _prototypeManager.EnumeratePrototypes()) + { + _applicableDamageGroups.Add(group); + _fullySupportedDamageGroups.Add(group); + } + foreach (var type in _prototypeManager.EnumeratePrototypes()) + { + _supportedDamageTypes.Add(type); + } return; } - foreach (var supportedClass in _supportedClasses) + // Add fully supported damage groups + foreach (var groupID in _supportedDamageGroupIDs) { - foreach (var supportedType in supportedClass.ToTypes()) + var group = _prototypeManager.Index(groupID); + _fullySupportedDamageGroups.Add(group); + foreach (var type in group.DamageTypes) { - _supportedTypes.Add(supportedType); + _supportedDamageTypes.Add(type); } } - foreach (var originalType in _supportedTypes) + // 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) { - _supportedClasses.Add(originalType.ToClass()); + var type = _prototypeManager.Index(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()) + { + 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()) + { + if (group.DamageTypes.Contains(type)) + { + _applicableDamageGroups.Add(group); + } + } } } } diff --git a/Content.Shared/Damage/DamageChangeData.cs b/Content.Shared/Damage/DamageChangeData.cs index 98b54f490d..fbe7a9fb8d 100644 --- a/Content.Shared/Damage/DamageChangeData.cs +++ b/Content.Shared/Damage/DamageChangeData.cs @@ -2,14 +2,14 @@ { /// /// Data class with information on how the value of a - /// single has changed. + /// single has changed. /// public struct DamageChangeData { /// /// Type of damage that changed. /// - public DamageType Type; + public DamageTypePrototype Type; /// /// The new current value for that damage. @@ -21,7 +21,7 @@ /// public int Delta; - public DamageChangeData(DamageType type, int newValue, int delta) + public DamageChangeData(DamageTypePrototype type, int newValue, int delta) { Type = type; NewValue = newValue; diff --git a/Content.Shared/Damage/DamageChangeParams.cs b/Content.Shared/Damage/DamageChangeParams.cs deleted file mode 100644 index 26c043d136..0000000000 --- a/Content.Shared/Damage/DamageChangeParams.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Content.Shared.Body.Components; -using Content.Shared.Damage.Components; - -namespace Content.Shared.Damage -{ - /// - /// Data class with information on how to damage a - /// . - /// While not necessary to damage for all instances, classes such as - /// may require it for extra data - /// (such as selecting which limb to target). - /// - // TODO BODY: Remove and pretend it never existed - public class DamageChangeParams : EventArgs - { - } -} diff --git a/Content.Shared/Damage/DamageChangedEventArgs.cs b/Content.Shared/Damage/DamageChangedEventArgs.cs index d7a3a7552d..0956bcdbf6 100644 --- a/Content.Shared/Damage/DamageChangedEventArgs.cs +++ b/Content.Shared/Damage/DamageChangedEventArgs.cs @@ -12,7 +12,7 @@ namespace Content.Shared.Damage Data = data; } - public DamageChangedEventArgs(IDamageableComponent damageable, DamageType type, int newValue, int delta) + public DamageChangedEventArgs(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta) { Damageable = damageable; @@ -28,7 +28,7 @@ namespace Content.Shared.Damage public IDamageableComponent Damageable { get; } /// - /// List containing data on each that was changed. + /// List containing data on each that was changed. /// public IReadOnlyList Data { get; } } diff --git a/Content.Shared/Damage/DamageChangedMessage.cs b/Content.Shared/Damage/DamageChangedMessage.cs index 5e7c2dc478..01cd546ee4 100644 --- a/Content.Shared/Damage/DamageChangedMessage.cs +++ b/Content.Shared/Damage/DamageChangedMessage.cs @@ -12,7 +12,7 @@ namespace Content.Shared.Damage Data = data; } - public DamageChangedMessage(IDamageableComponent damageable, DamageType type, int newValue, int delta) + public DamageChangedMessage(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta) { Damageable = damageable; @@ -28,7 +28,7 @@ namespace Content.Shared.Damage public IDamageableComponent Damageable { get; } /// - /// List containing data on each that was changed. + /// List containing data on each that was changed. /// public IReadOnlyList Data { get; } diff --git a/Content.Shared/Damage/DamageClass.cs b/Content.Shared/Damage/DamageClass.cs deleted file mode 100644 index b34671b487..0000000000 --- a/Content.Shared/Damage/DamageClass.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Robust.Shared.Serialization; - -namespace Content.Shared.Damage -{ - [Serializable, NetSerializable] - public enum DamageClass - { - Brute, - Burn, - Toxin, - Airloss, - Genetic - } - - public static class DamageClassExtensions - { - public static ImmutableList ToTypes(this DamageClass @class) - { - return DamageSystem.ClassToType[@class]; - } - - public static Dictionary ToNewDictionary() where T : struct - { - return Enum.GetValues(typeof(DamageClass)) - .Cast() - .ToDictionary(@class => @class, _ => default(T)); - } - - public static Dictionary ToNewDictionary() - { - return ToNewDictionary(); - } - } -} diff --git a/Content.Shared/Damage/DamageGroupPrototype.cs b/Content.Shared/Damage/DamageGroupPrototype.cs new file mode 100644 index 0000000000..d1463c7b59 --- /dev/null +++ b/Content.Shared/Damage/DamageGroupPrototype.cs @@ -0,0 +1,42 @@ +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 +{ + /// + /// A Group of s. + /// + /// + /// These groups can be used to specify supported damage types of a , or to change/get/set damage in a . + /// + [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 TypeIDs { get; } = default!; + + public HashSet DamageTypes { get; } = new(); + + // Create set of damage types + void ISerializationHooks.AfterDeserialization() + { + _prototypeManager = IoCManager.Resolve(); + + foreach (var typeID in TypeIDs) + { + DamageTypes.Add(_prototypeManager.Index(typeID)); + } + } + } +} diff --git a/Content.Shared/Damage/DamageSystem.cs b/Content.Shared/Damage/DamageSystem.cs index d2d7d7dc87..d40e0e8347 100644 --- a/Content.Shared/Damage/DamageSystem.cs +++ b/Content.Shared/Damage/DamageSystem.cs @@ -8,59 +8,6 @@ namespace Content.Shared.Damage [UsedImplicitly] public class DamageSystem : EntitySystem { - public static ImmutableDictionary> ClassToType { get; } = DefaultClassToType(); - public static ImmutableDictionary TypeToClass { get; } = DefaultTypeToClass(); - - private static ImmutableDictionary> DefaultClassToType() - { - return new Dictionary> - { - [DamageClass.Brute] = new List - { - DamageType.Blunt, - DamageType.Slash, - DamageType.Piercing - }.ToImmutableList(), - [DamageClass.Burn] = new List - { - DamageType.Heat, - DamageType.Shock, - DamageType.Cold - }.ToImmutableList(), - [DamageClass.Toxin] = new List - { - DamageType.Poison, - DamageType.Radiation - }.ToImmutableList(), - [DamageClass.Airloss] = new List - { - DamageType.Asphyxiation, - DamageType.Bloodloss - }.ToImmutableList(), - [DamageClass.Genetic] = new List - { - DamageType.Cellular - }.ToImmutableList() - }.ToImmutableDictionary(); - } - - private static ImmutableDictionary DefaultTypeToClass() - { - return new Dictionary - { - {DamageType.Blunt, DamageClass.Brute}, - {DamageType.Slash, DamageClass.Brute}, - {DamageType.Piercing, DamageClass.Brute}, - {DamageType.Heat, DamageClass.Burn}, - {DamageType.Shock, DamageClass.Burn}, - {DamageType.Cold, DamageClass.Burn}, - {DamageType.Poison, DamageClass.Toxin}, - {DamageType.Radiation, DamageClass.Toxin}, - {DamageType.Asphyxiation, DamageClass.Airloss}, - {DamageType.Bloodloss, DamageClass.Airloss}, - {DamageType.Cellular, DamageClass.Genetic} - }.ToImmutableDictionary(); - } } } diff --git a/Content.Shared/Damage/DamageType.cs b/Content.Shared/Damage/DamageType.cs deleted file mode 100644 index 65f1912f77..0000000000 --- a/Content.Shared/Damage/DamageType.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Robust.Shared.Serialization; - -namespace Content.Shared.Damage -{ - [Serializable, NetSerializable] - public enum DamageType - { - Blunt, - Slash, - Piercing, - Heat, - Shock, - Cold, - Poison, - Radiation, - Asphyxiation, - Bloodloss, - Cellular - } - - public static class DamageTypeExtensions - { - public static DamageClass ToClass(this DamageType type) - { - return DamageSystem.TypeToClass[type]; - } - - public static Dictionary ToNewDictionary() where T : struct - { - return Enum.GetValues(typeof(DamageType)) - .Cast() - .ToDictionary(type => type, _ => default(T)); - } - - public static Dictionary ToNewDictionary() - { - return ToNewDictionary(); - } - - public static Dictionary ToClassDictionary(this IReadOnlyDictionary types) - { - var classes = DamageClassExtensions.ToNewDictionary(); - - foreach (var @class in classes.Keys.ToList()) - { - foreach (var type in @class.ToTypes()) - { - if (!types.TryGetValue(type, out var damage)) - { - continue; - } - - classes[@class] += damage; - } - } - - return classes; - } - } -} diff --git a/Content.Shared/Damage/DamageTypePrototype.cs b/Content.Shared/Damage/DamageTypePrototype.cs new file mode 100644 index 0000000000..e39e44f059 --- /dev/null +++ b/Content.Shared/Damage/DamageTypePrototype.cs @@ -0,0 +1,18 @@ +using System; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Shared.Damage +{ + /// + /// A single damage type. These types are grouped together in s. + /// + [Prototype("damageType")] + [Serializable, NetSerializable] + public class DamageTypePrototype : IPrototype + { + [DataField("id", required: true)] + public string ID { get; } = default!; + } +} diff --git a/Content.Shared/Damage/Resistances/ResistanceSet.cs b/Content.Shared/Damage/Resistances/ResistanceSet.cs index 01016fab37..ba9c218494 100644 --- a/Content.Shared/Damage/Resistances/ResistanceSet.cs +++ b/Content.Shared/Damage/Resistances/ResistanceSet.cs @@ -1,5 +1,7 @@ -using System; +using System; using System.Collections.Generic; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -7,18 +9,21 @@ namespace Content.Shared.Damage.Resistances { /// /// Set of resistances used by damageable objects. - /// Each has a multiplier and flat damage + /// Each has a multiplier and flat damage /// reduction value. /// [Serializable, NetSerializable] public class ResistanceSet { + + [ViewVariables] + public string? ID { get; } = string.Empty; + + [ViewVariables] + public Dictionary Resistances { get; } = new(); + public ResistanceSet() { - foreach (var damageType in (DamageType[]) Enum.GetValues(typeof(DamageType))) - { - Resistances.Add(damageType, new ResistanceSetSettings(1f, 0)); - } } public ResistanceSet(ResistanceSetPrototype data) @@ -27,12 +32,6 @@ namespace Content.Shared.Damage.Resistances Resistances = data.Resistances; } - [ViewVariables] - public string ID { get; } = string.Empty; - - [ViewVariables] - public Dictionary Resistances { get; } = new(); - /// /// Adjusts input damage with the resistance set values. /// Only applies reduction if the amount is damage (positive), not @@ -40,11 +39,18 @@ namespace Content.Shared.Damage.Resistances /// /// Type of damage. /// Incoming amount of damage. - public int CalculateDamage(DamageType damageType, int amount) + 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 -= Resistances[damageType].FlatReduction; + amount -= resistance.FlatReduction; if (amount <= 0) { @@ -52,25 +58,9 @@ namespace Content.Shared.Damage.Resistances } } - amount = (int) Math.Ceiling(amount * Resistances[damageType].Coefficient); + amount = (int) Math.Ceiling(amount * resistance.Coefficient); return amount; } } - - /// - /// Settings for a specific damage type in a resistance set. Flat reduction is applied before the coefficient. - /// - [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; - } - } } diff --git a/Content.Shared/Damage/Resistances/ResistanceSetPrototype.cs b/Content.Shared/Damage/Resistances/ResistanceSetPrototype.cs index 854dd05463..2f4b64a696 100644 --- a/Content.Shared/Damage/Resistances/ResistanceSetPrototype.cs +++ b/Content.Shared/Damage/Resistances/ResistanceSetPrototype.cs @@ -1,5 +1,7 @@ -using System; +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; @@ -14,29 +16,46 @@ namespace Content.Shared.Damage.Resistances [Serializable, NetSerializable] public class ResistanceSetPrototype : IPrototype, ISerializationHooks { - [ViewVariables] - [DataField("coefficients")] - public Dictionary Coefficients { get; } = new(); - - [ViewVariables] - [DataField("flatReductions")] - public Dictionary FlatReductions { get; } = new(); - - [ViewVariables] - public Dictionary Resistances { get; private set; } = new(); - [ViewVariables] [DataField("id", required: true)] public string ID { get; } = default!; + [ViewVariables] + [DataField("coefficients", required: true)] + private Dictionary coefficients { get; } = new(); + + [ViewVariables] + [DataField("flatReductions", required: true)] + private Dictionary flatReductions { get; } = new(); + + [ViewVariables] + public Dictionary Resistances { get; private set; } = new(); + void ISerializationHooks.AfterDeserialization() { - Resistances = new Dictionary(); - foreach (var damageType in (DamageType[]) Enum.GetValues(typeof(DamageType))) + var prototypeManager = IoCManager.Resolve(); + foreach (var damageTypeID in coefficients.Keys) { - Resistances.Add(damageType, - new ResistanceSetSettings(Coefficients[damageType], FlatReductions[damageType])); + var resolvedDamageType = prototypeManager.Index(damageTypeID); + Resistances.Add(resolvedDamageType, new ResistanceSetSettings(coefficients[damageTypeID], flatReductions[damageTypeID])); } } } + + /// + /// Resistance Settings for a specific DamageType. Flat reduction should always be applied before the coefficient. + /// + [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; + } + } + } diff --git a/Content.Shared/MedicalScanner/SharedMedicalScannerComponent.cs b/Content.Shared/MedicalScanner/SharedMedicalScannerComponent.cs index 2d7af4a463..00b78ff6a1 100644 --- a/Content.Shared/MedicalScanner/SharedMedicalScannerComponent.cs +++ b/Content.Shared/MedicalScanner/SharedMedicalScannerComponent.cs @@ -16,25 +16,25 @@ namespace Content.Shared.MedicalScanner public class MedicalScannerBoundUserInterfaceState : BoundUserInterfaceState { public readonly EntityUid? Entity; - public readonly Dictionary DamageClasses; - public readonly Dictionary DamageTypes; + public readonly Dictionary DamagePerSupportedGroupID; + public readonly Dictionary DamagePerTypeID; public readonly bool IsScanned; public MedicalScannerBoundUserInterfaceState( EntityUid? entity, - Dictionary damageClasses, - Dictionary damageTypes, + Dictionary damagePerSupportedGroupID, + Dictionary damagePerTypeID, bool isScanned) { Entity = entity; - DamageClasses = damageClasses; - DamageTypes = damageTypes; + DamagePerSupportedGroupID = damagePerSupportedGroupID; + DamagePerTypeID = damagePerTypeID; IsScanned = isScanned; } public bool HasDamage() { - return DamageClasses.Count > 0 || DamageTypes.Count > 0; + return DamagePerSupportedGroupID.Count > 0 || DamagePerTypeID.Count > 0; } } diff --git a/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl b/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl index 46bfcc9710..11190183fd 100644 --- a/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl +++ b/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl @@ -16,5 +16,7 @@ medical-scanner-eject-verb-get-data-text = Eject medical-scanner-window-save-button-text = Scan and Save DNA medical-scanner-window-no-patient-data-text = No patient data. medical-scanner-window-entity-health-text = {$entityName}'s health: -medical-scanner-window-damage-class-text = {$damageClass}: {$amount} +medical-scanner-window-entity-damage-total-text = Total Damage: {$amount} +medical-scanner-window-damage-group-text = {$damageGroup}: {$amount} medical-scanner-window-damage-type-text = {$damageType}: {$amount} +medical-scanner-window-damage-type-duplicate-text = {$damageType}: {$amount} (duplicate) diff --git a/Resources/Prototypes/Body/Mechanisms/human.yml b/Resources/Prototypes/Body/Mechanisms/human.yml index 44652e4e65..08340d6775 100644 --- a/Resources/Prototypes/Body/Mechanisms/human.yml +++ b/Resources/Prototypes/Body/Mechanisms/human.yml @@ -124,36 +124,36 @@ Arithrazine: effects: - !type:HealthChange - damageClass: Toxin + damageGroup: Toxin healthChange: -1 - !type:HealthChange - damageClass: Brute + damageGroup: Brute healthChange: 0.5 Bicaridine: effects: - !type:HealthChange - damageClass: Brute + damageGroup: Brute healthChange: -2 Dermaline: effects: - !type:HealthChange - damageClass: Burn + damageGroup: Burn healthChange: -3 Dexalin: effects: - !type:HealthChange - damageClass: Airloss + damageGroup: Airloss healthChange: -1 DexalinPlus: effects: - !type:HealthChange - damageClass: Airloss + damageGroup: Airloss healthChange: -3 Dylovene: effects: - - !type:HealthChange - damageClass: Toxin - healthChange: -1 + - !type:HealthChange + damageGroup: Toxin + healthChange: -1 Ephedrine: effects: - !type:MovespeedModifier @@ -162,23 +162,23 @@ HeartbreakerToxin: effects: - !type:HealthChange - damageClass: Airloss + damageGroup: Airloss healthChange: 4 Kelotane: effects: - !type:HealthChange - damageClass: Burn + damageGroup: Burn healthChange: -1 Lexorin: effects: - !type:HealthChange - damageClass: Airloss + damageGroup: Airloss healthChange: 7 Meth: effects: - !type:HealthChange healthChange: 2.5 - damageClass: Toxin + damageGroup: Toxin - !type:MovespeedModifier walkSpeedModifier: 1.3 sprintSpeedModifier: 1.3 @@ -186,20 +186,20 @@ effects: - !type:HealthChange healthChange: -2 - damageClass: Burn + damageGroup: Burn - !type:HealthChange healthChange: -2 - damageClass: Toxin + damageGroup: Toxin - !type:HealthChange healthChange: -2 - damageClass: Airloss + damageGroup: Airloss - !type:HealthChange healthChange: -2 - damageClass: Brute + damageGroup: Brute Synaptizine: effects: - !type:HealthChange - damageClass: Toxin + damageGroup: Toxin healthChange: 0.5 - type: entity @@ -259,7 +259,7 @@ effects: - !type:SatiateThirst - !type:HealthChange - damageClass: Toxin + damageGroup: Toxin healthChange: 1 JuiceWatermelon: effects: @@ -304,7 +304,7 @@ - !type:SatiateThirst hydrationFactor: 2 - !type:HealthChange - damageClass: Toxin + damageGroup: Toxin healthChange: 1 - type: entity diff --git a/Resources/Prototypes/Damage/damage.yml b/Resources/Prototypes/Damage/damage.yml new file mode 100644 index 0000000000..139792242a --- /dev/null +++ b/Resources/Prototypes/Damage/damage.yml @@ -0,0 +1,97 @@ +# Silver: Todo break out into damage_type,damage_class, damage_container yml files when we support loading prototypes by priority. +- type: damageType + id: Blunt + +- type: damageType + id: Slash + +- type: damageType + id: Piercing + +- type: damageType + id: Heat + +- type: damageType + id: Shock + +- type: damageType + id: Cold + +# Poison damage. Generally caused by various reagents being metabolised. +- type: damageType + id: Poison + +- type: damageType + id: Radiation + +# Damage due to being unable to breathe. +# Represents not enough oxygen (or equivalent) getting to the blood. +# Usually healed automatically if entity can breathe +- type: damageType + id: Asphyxiation + +# Damage representing not having enough blood. +# Represents there not enough blood to supply oxygen (or equivalent). +- type: damageType + id: Bloodloss + +- type: damageType + id: Cellular + +- type: damageGroup + id: Brute + damageTypes: + - Blunt + - Slash + - Piercing + +- type: damageGroup + id: Burn + damageTypes: + - Heat + - Shock + - Cold + +# Airloss (sometimes called oxyloss) +# Caused by asphyxiation or bloodloss. +# Note that most medicine and damaging effects should probably modify either asphyxiation or +# bloodloss, not this whole group, unless you have a wonder drug that affects both. +- type: damageGroup + id: Airloss + damageTypes: + - Asphyxiation + - Bloodloss + +# As with airloss, most medicine and damage effects should probably modify either poison or radiation. +# Though there are probably some radioactive poisons. +- type: damageGroup + id: Toxin + damageTypes: + - Poison + - Radiation + + +- type: damageGroup + id: Genetic + damageTypes: + - Cellular + +- type: damageContainer + id: allDamageContainer + supportAll: true + + +- type: damageContainer + id: biologicalDamageContainer + supportedGroups: + - Brute + - Burn + - Toxin + - Airloss + - Genetic + +- type: damageContainer + id: metallicDamageContainer + supportedGroups: + - Brute + - Burn diff --git a/Resources/Prototypes/Damage/damage_containers.yml b/Resources/Prototypes/Damage/damage_containers.yml deleted file mode 100644 index e44898a109..0000000000 --- a/Resources/Prototypes/Damage/damage_containers.yml +++ /dev/null @@ -1,18 +0,0 @@ -- type: damageContainer - id: allDamageContainer - supportAll: true - -- type: damageContainer - id: biologicalDamageContainer - supportedClasses: - - Brute - - Burn - - Toxin - - Airloss - - Genetic - -- type: damageContainer - id: metallicDamageContainer - supportedClasses: - - Brute - - Burn diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 84fa813d13..289d99a7bd 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -34,6 +34,7 @@ - type: MovementSpeedModifier - type: MovedByPressure - type: Barotrauma + damageType: Blunt - type: DamageOnHighSpeedImpact soundHit: path: /Audio/Effects/hit_kick.ogg @@ -157,7 +158,9 @@ fireSpread: true canResistFire: true - type: Temperature + heatDamageType: Heat heatDamageThreshold: 360 + coldDamageType: Cold coldDamageThreshold: 260 currentTemperature: 310.15 specificHeat: 42 @@ -191,7 +194,7 @@ thresholds: - trigger: !type:DamageTypeTrigger - type: Blunt + damageType: Blunt damage: 400 behaviors: - !type:GibBehavior { } diff --git a/Resources/Prototypes/Entities/Mobs/Species/vox.yml b/Resources/Prototypes/Entities/Mobs/Species/vox.yml index 1f097c19a2..b85eaa1694 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/vox.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/vox.yml @@ -1,4 +1,4 @@ -# SKREEEEEEEEEEE +# Vox bad. moff best. - type: entity parent: HumanMob_Content abstract: True