From 32d99eaba93126ce2966d1b8c0b4f883b49a9c2c Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Wed, 25 Aug 2021 03:06:27 +1000 Subject: [PATCH] Damageable Refactor 3: Revenge of the Instamerge (#4524) * Add DamageType And DamageGroup Prototypes * Remove DamageTypePrototype Field "name" as its redundant * Change I/DamageableComponent to use prototypes * Update DamageContainer, ReisistanceSet and DamageChangeData * Change Barotrauma Component to use DamageType from DamageSystem * Update AsteroidRockComponent * update some more components * update some more components * Fix m o r e c o m p o n e n t s and their damageType * all thats left is bug/missing node hunting then verification. * push changes * update submodule * Merge fixes * push DGP for example * update damagecomponent across shared and server * fix a few bugs * Fix Merge issues * Refactor damageablecomponent update (#4406) * Fixing merge. I messed up part of the merge. this should fix it? * Barotrauma now uses prototypeManager As System.Runtime.CompilerServices also has a [Dependency], I think I had to use the full path [Robust.Shared.IoC.Dependency] * FlammableComponent now uses prototypeManager * SuicideCommands now use prototypeManager * Changed many files to use prototypeManager to resolve damaege prototypes Yeah.... prototype references would be very nice. maybe this was all a waste of time. * Grouping prototypeManager.Index with datafield definitions This will make it easier to eventually add prototype references * removed unused variable * Moved lines around. Lines now consistent with other TODO PROTOTYPE blocks * Grouping more prototypeManager.Index with datafield definitions * Removed unnecessary code * Added more prototypeManager indexing These ones weren't pointed out by DrSmug. But I think this is all of them? That or my regex is shit. * Remove redundant _damage field * Remove redundant _currentTemperature * Moved variables down * Added prototypeManager indexing to TemperatureComponent * WeaponComponent/System now use ProtptypeManager And as far as I can tell damageType is required, and therefore should never have been null anyway? * Make ranged weapon clumsy fire effects datafields And yes, the order in which the clumsy effects occur is very important. * Made damage on vital body part loss a datafield * Renamed several damageGroup variables to group * Capitalised DamageListToDamageGroup * Make radiation and explosion damage types datafields * Renamed _supportedDamageGroupIDs and _supportedDamageTypeIDs * Fixed mistakes Frogot to remove prototypeManager index DamageTypeTrigger, and wrong variable visibility in TemperatureComponent * Added necessary code Is something tragically wrong? * MeleeWeapon damageType is not actually required * Fixing someone else's mistakes A search comes up with nothing in the yaml files, and its not a required field. So no one uses it? Hopefully? * Changed and renamed damageTypeToDamageGroup Previously would incorrectly return the total container damage for each group, not the total in the group * renaming varitables * Renamed variable DamageClasses * Added dictionary converting functions * Added ID-keyed dictionaries * Making MedicalScanner use ID dictionaries, instead of prototype dictionaries Oh oh no. I've been able to avoid UI & networking up until now. I have no Idea what I am doing. * Fix Medical Scanner * Summary (required) The joke here is that this fixes the empty summary. * Removed DamageableComponent.GetDamageGroup/Type * Renamed "damage classes" to groups. * Update ChangeDamage description * Replaced Heal() with SettAllDamage() Heal() was just a confusing name, * More Class -> Group renaming * Replace Class with Group in yaml files DamageClassTrigger does not appear in any yaml? only in testing? DamageTypeTrigger appears only in human.yaml? HealthChangeMetabolism is Mostly in medicine.yml and one in soad.yaml Why the hell is Cola metabolizable by plants? Who is pouring cola on their plants!?!? * Fix _prototypeManager being null errors. * Changing comments Where are the prototype references * MetabolismComponent doesn't give free heals anymore. * Changes HungerComponent healing. Previously I think it would actually damage you. Only did this as I though it was causing the fast healing. Turns out that was just BREATHING. * Generalised a function in DamageableComponent and moved it to DamageGroupPrototype previously DamageTypesDictToDamageGroupDict was private to DamageableComponent, but was also quite general (nearly a static function). As this sort of function may be needed by other components using DamageGroupPrototypes in the future, I moved it there as a static function instead. * modified DamageableComponent.ChangeDamage() ignoreResistances was renamed to ignoreDamageResistances to make it clearer that it had no effect on healing. Now uses default argument for ignoreDamageResistances, so when healing you are not forced to specify an argument that does nothing. Also made some general changes to ignoreResistances() * Changed class->group and added missing damage type functionality to DamageContainerPrototypes * Added Comments to damage.yml * Misc Changes to DamageableComponent * Differentiated between group support and group applicability So far, every damage type is a member of one, and only one, damage group. So this change has no real effect so far. * Added proposed alternative to ChangeDamage() * fixed error in DamageGroupPrototype * Changes to DamageableComponent Lots of changes to comments. Some variables renamed in IDamageableComponent and DamageableComponent (also required renaming in other files) Some minor logic changes, mostly for incorrect descirptions of boolean return values. Also further differentiating between ApplicableGroups and SupportedGroups... if that will ever even matter * Generalised MedicalScannerComponent If needed, can print miscellaneous damage types now * Fixed HealthChangeMetabolism bug * Changing Comments around * More questions * Made Barotrauma default to blunt * Fix RejuvenateTest.cs * Comments * Coments and variable names * fix some master-merge issues * Removed redundant fields * Misc changes for readbility of PR diff * Consistent naming * Fixed atmos damage bug * Removed Ranting * Fixed Hunger after I broke it * Fixing Bugs * Removed stupid question * Removed more stupid questions * Fix potential null errors. * Made boolean return values consistent Also renamed several functions, to make it clear they return a bool. Docs were also updated. * Removed IoCManager.InjectDependencies() * Removed unnecessary 'suffocation' prefix * Fixed Spelling Also removed accidentally left in logger call * Fixed Medical Scanner * Apply suggestions from code review Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> * Changing comments and whitespaces * Made damage thresholds trigger datafields required * So many typos * Changes to DamageableComponents Changed documentation in IDamageableComponent Made testing code more readable. Relabelled groups as 'Applicable' either 'Fully Supported' * Removed function and degeneralised * Update DamageableComponent.cs Removed unused parameters Fixed Networking * Added IoCManager.Resolve * Now using alternative TryChangeDamage() * Removed function from DamageGroupPrototype * Removing comments * Remove bad if statement? * Fix damageChanged ordering * Fix hurt server command * Changed //TODO PROTOTYPE blocks Now use PrototypeManager differently. Wherever possible, only retrieve the prototype once. Also added default damage types to some more datafields * Update Content.Shared/Damage/Container/DamageContainerPrototype.cs Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> * renamed _accumulatedHealth -> _accumulatedDamage and added TODOs * Another class-> group * Fix bug in generalisation of damage container prototypes * Addes Tests to make sure I dont keep adding bugs to my own code. * Changed Return values when setting * Removed unused class * Added more tests, split tests into three files * Made damage types public and VV read-write-able * Minor changes to DamageableComponent Replaced internal use of GetDamagePerType with _damageDict and removed some unnecessary fields * Fix Suicide, by adding IoC Resolve() * Fix DamageGroupTrigger bug * Fix typos in tests * Change comments./docstrings & spacing * Merge tests, use test prototypes Co-authored-by: Leon Friedrich <60421075+leonsfriedrich@users.noreply.github.com> Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> * Fix merge issues Co-authored-by: Silver Co-authored-by: DrSmugleaf Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> Co-authored-by: Leon Friedrich <60421075+leonsfriedrich@users.noreply.github.com> --- .../MedicalScanner/UI/MedicalScannerWindow.cs | 60 ++- .../Tests/Commands/RejuvenateTest.cs | 8 +- .../Damageable/AllSupportDamageableTest.cs | 121 +++++ .../Tests/Damageable/DamageableTest.cs | 429 ++++++++++++---- ...Test.cs => DestructibleDamageGroupTest.cs} | 53 +- .../DestructibleDamageTypeTest.cs | 36 +- .../DestructibleDestructionTest.cs | 7 +- .../DestructibleTestPrototypes.cs | 106 +++- .../DestructibleThresholdActivationTest.cs | 34 +- .../Atmos/Components/BarotraumaComponent.cs | 26 +- .../Atmos/Components/FlammableComponent.cs | 16 +- .../Body/BodyManagerHealthChangeParams.cs | 4 +- .../Body/Respiratory/RespiratorComponent.cs | 26 +- .../Chat/Commands/SuicideCommand.cs | 30 +- .../Chemistry/ReagentEffects/HealthChange.cs | 47 +- Content.Server/Damage/Commands/HurtCommand.cs | 43 +- .../DamageOnHighSpeedImpactComponent.cs | 17 +- .../Components/DamageOnLandComponent.cs | 26 +- .../DamageOnToolInteractComponent.cs | 39 +- .../Components/DamageOtherOnHitComponent.cs | 16 +- .../Damage/DamageOnHighSpeedImpactSystem.cs | 2 +- .../Damage/DamageOtherOnHitSystem.cs | 2 +- Content.Server/Damage/GodmodeSystem.cs | 34 +- Content.Server/Damage/RejuvenateVerb.cs | 2 +- .../Thresholds/Triggers/DamageClassTrigger.cs | 39 -- .../Thresholds/Triggers/DamageGroupTrigger.cs | 42 ++ .../Thresholds/Triggers/DamageTrigger.cs | 6 +- .../Thresholds/Triggers/DamageTypeTrigger.cs | 20 +- .../Doors/Components/ServerDoorComponent.cs | 16 +- .../GameTicking/Presets/GamePreset.cs | 4 +- .../Presets/PresetTraitorDeathMatch.cs | 6 +- .../Light/Components/PoweredLightComponent.cs | 23 +- .../Medical/Components/HealingComponent.cs | 17 +- .../Components/MedicalScannerComponent.cs | 13 +- .../Components/AsteroidRockComponent.cs | 13 +- .../Nutrition/Components/HungerComponent.cs | 41 +- .../Nutrition/Components/ThirstComponent.cs | 32 +- .../Nutrition/EntitySystems/HungerSystem.cs | 2 +- .../Components/HitscanComponent.cs | 31 +- .../Components/ProjectileComponent.cs | 14 +- .../Projectiles/ProjectileSystem.cs | 9 +- .../Repairable/RepairableComponent.cs | 2 +- .../Components/SpawnPointComponent.cs | 2 +- .../Components/TemperatureComponent.cs | 71 +-- .../Melee/Components/MeleeWeaponComponent.cs | 18 +- .../Weapon/Melee/MeleeWeaponSystem.cs | 6 +- .../ServerBatteryBarrelComponent.cs | 2 +- .../Components/ServerRangedBarrelComponent.cs | 2 +- .../Ranged/ServerRangedWeaponComponent.cs | 44 +- .../Body/Components/SharedBodyComponent.cs | 20 +- .../Damage/Components/DamageableComponent.cs | 462 +++++++++++------- .../Damage/Components/IDamageableComponent.cs | 229 ++++++--- .../Container/DamageContainerPrototype.cs | 132 ++++- Content.Shared/Damage/DamageChangeData.cs | 6 +- Content.Shared/Damage/DamageChangeParams.cs | 18 - .../Damage/DamageChangedEventArgs.cs | 4 +- Content.Shared/Damage/DamageChangedMessage.cs | 4 +- Content.Shared/Damage/DamageClass.cs | 38 -- Content.Shared/Damage/DamageGroupPrototype.cs | 42 ++ Content.Shared/Damage/DamageSystem.cs | 53 -- Content.Shared/Damage/DamageType.cs | 63 --- Content.Shared/Damage/DamageTypePrototype.cs | 18 + .../Damage/Resistances/ResistanceSet.cs | 52 +- .../Resistances/ResistanceSetPrototype.cs | 51 +- .../SharedMedicalScannerComponent.cs | 14 +- .../components/medical-scanner-component.ftl | 4 +- .../Prototypes/Body/Mechanisms/human.yml | 40 +- Resources/Prototypes/Damage/damage.yml | 97 ++++ .../Prototypes/Damage/damage_containers.yml | 18 - .../Entities/Mobs/Species/human.yml | 5 +- .../Prototypes/Entities/Mobs/Species/vox.yml | 2 +- 71 files changed, 2048 insertions(+), 983 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Damageable/AllSupportDamageableTest.cs rename Content.IntegrationTests/Tests/Destructible/{DestructibleDamageClassTest.cs => DestructibleDamageGroupTest.cs} (72%) delete mode 100644 Content.Server/Destructible/Thresholds/Triggers/DamageClassTrigger.cs create mode 100644 Content.Server/Destructible/Thresholds/Triggers/DamageGroupTrigger.cs delete mode 100644 Content.Shared/Damage/DamageChangeParams.cs delete mode 100644 Content.Shared/Damage/DamageClass.cs create mode 100644 Content.Shared/Damage/DamageGroupPrototype.cs delete mode 100644 Content.Shared/Damage/DamageType.cs create mode 100644 Content.Shared/Damage/DamageTypePrototype.cs create mode 100644 Resources/Prototypes/Damage/damage.yml delete mode 100644 Resources/Prototypes/Damage/damage_containers.yml 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