ECS damageable (#4529)

* ECS and damage Data

* Comments and newlines

* Added Comments

* Make TryChangeDamageEvent immutable

* Remove SetAllDamage event

Use public SetAllDamage function instead

* Undo destructible mistakes

That was some shit code.

* Rename DamageData to DamageSpecifier

And misc small edits

misc

* Cache trigger prototypes.

* Renaming destructible classes & functions

* Revert "Cache trigger prototypes."

This reverts commit 86bae15ba6616884dba75f552dfdfbe2d1fb6586.

* Replace prototypes with prototype IDs.

* Split damage.yml into individual files

* move get/handle component state to system

* Update HealthChange doc

* Make godmode call Dirty() on damageable component

* Add Initialize() to fix damage test

* Make non-static

* uncache resistance set prototype and trim DamageableComponentState

* Remove unnecessary Dirty() calls during initialization

* RemoveTryChangeDamageEvent

* revert Dirty()

* Fix MobState relying on DamageableComponent.Dirty()

* Fix DisposalUnit Tests.

These were previously failing, but because the async was not await-ed, this never raised the exception.

After I fixed MobState component, this exception stopped happening and instead the assertions started being tested & failing

* Disposal test 2: electric boogaloo

* Fix typos/mistakes

also add comments and fix spacing.

* Use Uids instead of IEntity

* fix merge

* Comments, a merge issue, and making some damage ignore resistances

* Extend DamageSpecifier and use it for DamageableComponent

* fix master merge

* Fix Disposal unit test. Again.

Snapgrids were removed in master

* Execute Exectute
This commit is contained in:
Leon Friedrich
2021-09-15 03:07:37 +10:00
committed by GitHub
parent 22cc42ff50
commit df584ad446
212 changed files with 2876 additions and 3441 deletions

View File

@@ -2,7 +2,7 @@ using System.Linq;
using Content.Shared.Body.Components;
using Content.Shared.Body.Mechanism;
using Content.Shared.Body.Part;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
@@ -147,7 +147,7 @@ namespace Content.Client.Body.UI
BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Owner.Name)}";
// TODO BODY Part damage
if (part.Owner.TryGetComponent(out IDamageableComponent? damageable))
if (part.Owner.TryGetComponent(out DamageableComponent? damageable))
{
BodyPartHealth.Text = Loc.GetString("body-scanner-display-body-part-damage-text",("damage", damageable.TotalDamage));
}

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Client.HealthOverlay.UI;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.GameTicking;
using Content.Shared.MobState;
using JetBrains.Annotations;
@@ -79,7 +79,7 @@ namespace Content.Client.HealthOverlay
var viewBox = _eyeManager.GetWorldViewport().Enlarged(2.0f);
foreach (var (mobState, _) in ComponentManager.EntityQuery<IMobStateComponent, IDamageableComponent>())
foreach (var (mobState, _) in ComponentManager.EntityQuery<IMobStateComponent, DamageableComponent>())
{
var entity = mobState.Owner;

View File

@@ -1,6 +1,6 @@
using Content.Client.IoC;
using Content.Client.Resources;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.MobState;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -76,7 +76,7 @@ namespace Content.Client.HealthOverlay.UI
}
if (!Entity.TryGetComponent(out IMobStateComponent? mobState) ||
!Entity.TryGetComponent(out IDamageableComponent? damageable))
!Entity.TryGetComponent(out DamageableComponent? damageable))
{
CritBar.Visible = false;
HealthBar.Visible = false;

View File

@@ -1,6 +1,5 @@
using System.Text;
using System.Collections.Generic;
using Content.Shared.Damage;
using System.Linq;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
@@ -10,6 +9,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using static Content.Shared.MedicalScanner.SharedMedicalScannerComponent;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Content.Shared.Damage.Prototypes;
namespace Content.Client.MedicalScanner.UI
{
@@ -55,59 +55,36 @@ namespace Content.Client.MedicalScanner.UI
{
text.Append($"{Loc.GetString("medical-scanner-window-entity-health-text", ("entityName", entity.Name))}\n");
// Show the total damage
var totalDamage = state.DamagePerTypeID.Values.Sum();
var totalDamage = state.DamagePerType.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<string> shownTypeIDs = new();
HashSet<string> shownTypes = 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)
// Show the total damage and type breakdown for each damage group.
foreach (var (damageGroupID, damageAmount) in state.DamagePerGroup)
{
// 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.
// Show the damage for each type in that group.
var group = IoCManager.Resolve<IPrototypeManager>().Index<DamageGroupPrototype>(damageGroupID);
foreach (var type in group.DamageTypes)
{
if (state.DamagePerTypeID.TryGetValue(type.ID, out var typeAmount))
if (state.DamagePerType.TryGetValue(type, out var typeAmount))
{
// 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))
if (!shownTypes.Contains(type))
{
shownTypeIDs.Add(type.ID);
text.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-text", ("damageType", type.ID), ("amount", typeAmount))}");
shownTypes.Add(type);
text.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-text", ("damageType", type), ("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-duplicate-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;

View File

@@ -1,14 +0,0 @@
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
namespace Content.Client.MobState
{
[RegisterComponent]
[ComponentReference(typeof(SharedMobStateComponent))]
[ComponentReference(typeof(IMobStateComponent))]
public class MobStateComponent : SharedMobStateComponent
{
}
}

View File

@@ -1,7 +1,7 @@
using System.Threading.Tasks;
using Content.Server.Damage;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.MobState;
using NUnit.Framework;
using Robust.Shared.GameObjects;
@@ -21,7 +21,7 @@ namespace Content.IntegrationTests.Tests.Commands
id: DamageableDummy
components:
- type: Damageable
damageContainer: biologicalDamageContainer
damageContainer: Biological
- type: MobState
thresholds:
0: !type:NormalMobState {}
@@ -47,15 +47,17 @@ namespace Content.IntegrationTests.Tests.Commands
var human = entityManager.SpawnEntity("DamageableDummy", MapCoordinates.Nullspace);
// Sanity check
Assert.True(human.TryGetComponent(out IDamageableComponent damageable));
Assert.True(human.TryGetComponent(out DamageableComponent damageable));
Assert.True(human.TryGetComponent(out IMobStateComponent mobState));
mobState.UpdateState(0);
Assert.That(mobState.IsAlive, Is.True);
Assert.That(mobState.IsCritical, Is.False);
Assert.That(mobState.IsDead, Is.False);
Assert.That(mobState.IsIncapacitated, Is.False);
// Kill the entity
damageable.TryChangeDamage(prototypeManager.Index<DamageGroupPrototype>("Toxin"), 10000000, true);
DamageSpecifier damage = new(prototypeManager.Index<DamageGroupPrototype>("Toxin"), 10000000);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(human.Uid, damage, true);
// Check that it is dead
Assert.That(mobState.IsAlive, Is.False);

View File

@@ -1,121 +0,0 @@
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";
/// <summary>
/// Test a damageContainer with all types supported.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[Test]
public async Task TestAllSupportDamageableComponent()
{
var server = StartServerDummyTicker();
await server.WaitIdleAsync();
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
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<IDamageableComponent>();
});
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<DamageTypePrototype>().ToList().Count, Is.GreaterThan(0));
Assert.That(sPrototypeManager.EnumeratePrototypes<DamageGroupPrototype>().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<DamageTypePrototype>())
{
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<DamageGroupPrototype>())
{
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);
}
});
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -11,75 +11,76 @@ namespace Content.IntegrationTests.Tests.Damageable
{
[TestFixture]
[TestOf(typeof(DamageableComponent))]
[TestOf(typeof(DamageableSystem))]
public class DamageableTest : ContentIntegrationTest
{
private const string DamageableEntityId = "TestDamageableEntityId";
private const string Group1Id = "TestGroup1";
private const string Group2Id = "TestGroup2";
private const string Group3Id = "TestGroup3";
private string Prototypes = $@"
private const string Prototypes = @"
# Define some damage groups
- type: damageType
id: TestDamage11
id: TestDamage1
- type: damageType
id: TestDamage21
id: TestDamage2a
- type: damageType
id: TestDamage22
id: TestDamage2b
- type: damageType
id: TestDamage31
id: TestDamage3a
- type: damageType
id: TestDamage32
id: TestDamage3b
- type: damageType
id: TestDamage33
id: TestDamage3c
# Define damage Groups with 1,2,3 damage types
- type: damageGroup
id: {Group1Id}
id: TestGroup1
damageTypes:
- TestDamage11
- TestDamage1
- type: damageGroup
id: {Group2Id}
id: TestGroup2
damageTypes:
- TestDamage21
- TestDamage22
- TestDamage2a
- TestDamage2b
- type: damageGroup
id: {Group3Id}
id: TestGroup3
damageTypes:
- TestDamage31
- TestDamage32
- TestDamage33
- TestDamage3a
- TestDamage3b
- TestDamage3c
# 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: resistanceSet
id: testResistances
# this space is intentionally left blank
# This container should not support TestDamage1 or TestDamage2b
- type: damageContainer
id: testSomeDamageContainer
id: testDamageContainer
defaultResistanceSet: testResistances
supportedGroups:
- {Group3Id}
- TestGroup3
supportedTypes:
- TestDamage21
- TestDamage22
- TestDamage2a
- type: entity
id: {DamageableEntityId}
name: {DamageableEntityId}
id: TestDamageableEntityId
name: TestDamageableEntityId
components:
- type: Damageable
damageContainer: testSomeDamageContainer
damageContainer: testDamageContainer
";
/// <summary>
/// Test a standard damageable components
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
// public bool & function to determine whether dealing damage resulted in actual damage change
public bool DamageChanged = false;
public void DamageChangedListener(EntityUid _, DamageableComponent comp, DamageChangedEvent args)
{
DamageChanged = true;
}
[Test]
public async Task TestDamageableComponents()
{
@@ -93,360 +94,150 @@ namespace Content.IntegrationTests.Tests.Damageable
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDamageableEntity;
IDamageableComponent sDamageableComponent = null;
sEntityManager.EventBus.SubscribeLocalEvent<DamageableComponent, DamageChangedEvent>(DamageChangedListener);
IEntity sDamageableEntity = null;
DamageableComponent sDamageableComponent = null;
DamageableSystem sDamageableSystem = null;
DamageGroupPrototype group1 = default!;
DamageGroupPrototype group2 = default!;
DamageGroupPrototype group3 = default!;
DamageTypePrototype type1 = default!;
DamageTypePrototype type2a = default!;
DamageTypePrototype type2b = default!;
DamageTypePrototype type3a = default!;
DamageTypePrototype type3b = default!;
DamageTypePrototype type3c = default!;
int typeDamage, groupDamage;
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<IDamageableComponent>();
sDamageableEntity = sEntityManager.SpawnEntity("TestDamageableEntityId", coordinates);
sDamageableComponent = sDamageableEntity.GetComponent<DamageableComponent>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
group1 = sPrototypeManager.Index<DamageGroupPrototype>(Group1Id);
group2 = sPrototypeManager.Index<DamageGroupPrototype>(Group2Id);
group3 = sPrototypeManager.Index<DamageGroupPrototype>(Group3Id);
group1 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup1");
group2 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup2");
group3 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup3");
type1 = sPrototypeManager.Index<DamageTypePrototype>("TestDamage1");
type2a = sPrototypeManager.Index<DamageTypePrototype>("TestDamage2a");
type2b = sPrototypeManager.Index<DamageTypePrototype>("TestDamage2b");
type3a = sPrototypeManager.Index<DamageTypePrototype>("TestDamage3a");
type3b = sPrototypeManager.Index<DamageTypePrototype>("TestDamage3b");
type3c = sPrototypeManager.Index<DamageTypePrototype>("TestDamage3c");
});
await server.WaitRunTicks(5);
await server.WaitAssertion(() =>
{
// 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 uid = sDamageableEntity.Uid;
// Check that the correct types are supported:
foreach (var group in sPrototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
{
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);
}
}
}
// Check that the correct types are supported.
Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type1.ID), Is.False);
Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type2a.ID), Is.True);
Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type2b.ID), Is.False);
Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type3a.ID), Is.True);
Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type3b.ID), Is.True);
Assert.That(sDamageableComponent.Damage.DamageDict.ContainsKey(type3c.ID), Is.True);
// Check that damage is evenly distributed over a group if its a nice multiple
var types = group3.DamageTypes;
var damageToDeal = types.Count() * 5;
DamageSpecifier damage = new(group3, damageToDeal);
Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group1), Is.False);
Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group2), Is.True);
Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group3), Is.True);
// Check that damage works properly if perfectly divisible among group members
int damageToDeal, groupDamage, typeDamage; ;
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
foreach (var damageGroup in sDamageableComponent.FullySupportedDamageGroups)
{
var types = damageGroup.DamageTypes;
// Damage
damageToDeal = types.Count() * 5;
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.True);
sDamageableSystem.TryChangeDamage(uid, damage, true);
Assert.That(DamageChanged);
DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal));
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True);
Assert.That(groupDamage, Is.EqualTo(damageToDeal));
Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(damageToDeal));
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True);
Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage));
Assert.That(typeDamage, Is.EqualTo(damageToDeal / types.Count()));
}
// Heal
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, -damageToDeal, true), Is.True);
sDamageableSystem.TryChangeDamage(uid, -damage);
Assert.That(DamageChanged);
DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.Zero);
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True);
Assert.That(groupDamage, Is.Zero);
Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(0));
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True);
Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage));
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
types = group3.DamageTypes;
damageToDeal = types.Count() * 5 - 1;
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.True);
damage = new DamageSpecifier(group3, damageToDeal);
sDamageableSystem.TryChangeDamage(uid, damage, true);
Assert.That(DamageChanged);
DamageChanged = false;
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));
}
Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(damageToDeal));
// integer rounding. In this case, first member gets 1 less than others.
Assert.That(sDamageableComponent.Damage.DamageDict[type3a.ID], Is.EqualTo(damageToDeal / types.Count()));
Assert.That(sDamageableComponent.Damage.DamageDict[type3b.ID], Is.EqualTo(1 + damageToDeal / types.Count()));
Assert.That(sDamageableComponent.Damage.DamageDict[type3c.ID], Is.EqualTo(1 + damageToDeal / types.Count()));
// Heal
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, -damageToDeal, true), Is.True);
sDamageableSystem.TryChangeDamage(uid, -damage);
Assert.That(DamageChanged);
DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.Zero);
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True);
Assert.That(groupDamage, Is.Zero);
Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(0));
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True);
Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage));
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<DamageGroupPrototype>())
{
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?
damage = new DamageSpecifier(group1, 10) + new DamageSpecifier(type2b, 10);
sDamageableSystem.TryChangeDamage(uid, damage, true);
Assert.That(DamageChanged, Is.False);
Assert.That(sDamageableComponent.DamagePerGroup.TryGetValue(group1.ID, out groupDamage), Is.False);
Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type1.ID, out typeDamage), Is.False);
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));
// Test SetAll function
sDamageableSystem.SetAllDamage(sDamageableComponent, 10);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(10 * sDamageableComponent.Damage.DamageDict.Count()));
sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
// Test preferential healing
damageToDeal = 12;
var damageTypes = group3.DamageTypes.ToArray();
// Test 'wasted' healing
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3a, 5));
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3b, 7));
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -11));
Assert.That(sDamageableComponent.Damage.DamageDict[type3a.ID], Is.EqualTo(2));
Assert.That(sDamageableComponent.Damage.DamageDict[type3b.ID], Is.EqualTo(3));
Assert.That(sDamageableComponent.Damage.DamageDict[type3c.ID], Is.EqualTo(0));
// 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
";
/// <summary>
/// Generalized damageable component tests.
/// </summary>
/// <remarks>
/// Test scenarios where damage types are members of more than one group, or where a component only supports a subset of a group.
/// </remarks>
[Test]
public async Task TestGeneralizedDamageableComponent()
{
var server = StartServerDummyTicker(new ServerContentIntegrationOption
{
ExtraPrototypes = Prototypes2
});
await server.WaitIdleAsync();
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
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();
var coordinates = new MapCoordinates(0, 0, mapId);
sMapManager.CreateMap(mapId);
sDamageableEntity = sEntityManager.SpawnEntity(DamageableEntityId, coordinates);
sDamageableComponent = sDamageableEntity.GetComponent<IDamageableComponent>();
group1 = sPrototypeManager.Index<DamageGroupPrototype>(Group1Id);
group2 = sPrototypeManager.Index<DamageGroupPrototype>(Group2Id);
group3 = sPrototypeManager.Index<DamageGroupPrototype>(Group3Id);
SharedDamageType = sPrototypeManager.Index<DamageTypePrototype>(SharedDamageTypeId);
UnsupportedDamageType = sPrototypeManager.Index<DamageTypePrototype>(UnsupportedDamageTypeId);
});
await server.WaitRunTicks(5);
await server.WaitAssertion(() =>
{
// 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);
// 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);
// Check that the correct damage types are supported
Assert.That(sDamageableComponent.IsSupportedDamageType(SharedDamageType), Is.True);
// 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));
// 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));
// Test Over-Healing
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -100));
Assert.That(DamageChanged);
DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
// Test that if no health change occurred, returns false
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -100));
Assert.That(DamageChanged, Is.False);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
});
}
}

View File

@@ -1,7 +1,7 @@
using System.Threading.Tasks;
using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -21,11 +21,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{
var server = StartServerDummyTicker(new ServerContentIntegrationOption
{
ExtraPrototypes = Prototypes,
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
ExtraPrototypes = Prototypes
});
await server.WaitIdleAsync();
@@ -33,10 +29,12 @@ namespace Content.IntegrationTests.Tests.Destructible
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity;
IDamageableComponent sDamageableComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null;
IEntity sDestructibleEntity = null;
DamageableComponent sDamageableComponent = null;
TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
DamageableSystem sDamageableSystem = null;
await server.WaitPost(() =>
{
@@ -45,15 +43,16 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageGroupEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>();
sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
});
await server.WaitRunTicks(5);
await server.WaitAssertion(() =>
{
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
});
await server.WaitAssertion(() =>
@@ -61,26 +60,29 @@ namespace Content.IntegrationTests.Tests.Destructible
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute");
var burnDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBurn");
DamageSpecifier bruteDamage = new(bruteDamageGroup,5);
DamageSpecifier burnDamage = new(burnDamageGroup,5);
// Raise brute damage to 5
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 5, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage, true);
// No thresholds reached yet, the earliest one is at 10 damage
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 5, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage, true);
// No threshold reached, burn needs to be 10 as well
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, burnDamage * 2, true);
// One threshold reached, brute 10 + burn 10
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
// Threshold brute 10 + burn 10
var msg = sThresholdListenerComponent.ThresholdsReached[0];
var msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var threshold = msg.Threshold;
// Check that it matches the YAML prototype
@@ -94,55 +96,55 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[0]);
Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Raise brute damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, burnDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Lower brute damage to 0
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -20, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * -10);
Assert.That(sDamageableComponent.TotalDamage,Is.EqualTo(20));
// No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage back up to 10
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * 2, true);
// 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));
// 10 brute + 10 burn threshold reached, brute was healed and brought back to its threshold amount and slash stayed the same
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal both classes of damage to 0
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -10, true));
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, -20, true));
sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, burnDamage * 2, 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));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
// Threshold brute 10 + burn 10
msg = sThresholdListenerComponent.ThresholdsReached[0];
msg = sTestThresholdListenerSystem.ThresholdsReached[0];
threshold = msg.Threshold;
// Check that it matches the YAML prototype
@@ -156,29 +158,28 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[0]);
Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Change triggers once to true
threshold.TriggersOnce = true;
// Heal brute and burn back to 0
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -10, true));
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, -10, true));
sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// No new thresholds reached from healing
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, burnDamage * 2, true);
// No new thresholds reached as triggers once is set to true and it already triggered before
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
});
}
}

View File

@@ -1,7 +1,7 @@
using System.Threading.Tasks;
using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -21,21 +21,19 @@ namespace Content.IntegrationTests.Tests.Destructible
{
var server = StartServerDummyTicker(new ServerContentIntegrationOption
{
ExtraPrototypes = Prototypes,
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
ExtraPrototypes = Prototypes
});
await server.WaitIdleAsync();
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity;
IDamageableComponent sDamageableComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null;
IEntity sDestructibleEntity = null;
DamageableComponent sDamageableComponent = null;
TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
DamageableSystem sDamageableSystem = null;
await server.WaitPost(() =>
{
@@ -44,15 +42,16 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageTypeEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>();
sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
});
await server.WaitRunTicks(5);
await server.WaitAssertion(() =>
{
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
});
await server.WaitAssertion(() =>
@@ -60,26 +59,29 @@ namespace Content.IntegrationTests.Tests.Destructible
var bluntDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("TestBlunt");
var slashDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("TestSlash");
var bluntDamage = new DamageSpecifier(bluntDamageType,5);
var slashDamage = new DamageSpecifier(slashDamageType,5);
// Raise blunt damage to 5
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 5, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage, true);
// No thresholds reached yet, the earliest one is at 10 damage
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 5, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage, true);
// No threshold reached, slash needs to be 10 as well
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * 2, true);
// One threshold reached, blunt 10 + slash 10
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
// Threshold blunt 10 + slash 10
var msg = sThresholdListenerComponent.ThresholdsReached[0];
var msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var threshold = msg.Threshold;
// Check that it matches the YAML prototype
@@ -93,55 +95,55 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[0]);
Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Raise blunt damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Lower blunt damage to 0
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -20, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * -4, true);
// No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage back up to 10
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * 2, 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));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal both types of damage to 0
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -10, true));
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, -20, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * -2, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * -4, true);
// No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * 2, 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));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
// Threshold blunt 10 + slash 10
msg = sThresholdListenerComponent.ThresholdsReached[0];
msg = sTestThresholdListenerSystem.ThresholdsReached[0];
threshold = msg.Threshold;
// Check that it matches the YAML prototype
@@ -155,29 +157,29 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[0]);
Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Change triggers once to true
threshold.TriggersOnce = true;
// Heal blunt and slash back to 0
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -10, true));
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, -10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * -2, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * -2, true);
// No new thresholds reached from healing
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * 2, true);
// No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * 2, true);
// No new thresholds reached as triggers once is set to true and it already triggered before
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
});
}
}

View File

@@ -3,7 +3,7 @@ using System.Threading.Tasks;
using Content.Server.Destructible.Thresholds;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -20,11 +20,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{
var server = StartServerDummyTicker(new ServerContentIntegrationOption
{
ExtraPrototypes = Prototypes,
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
ExtraPrototypes = Prototypes
});
await server.WaitIdleAsync();
@@ -32,10 +28,11 @@ namespace Content.IntegrationTests.Tests.Destructible
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity = null;
IDamageableComponent sDamageableComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null;
DamageableComponent sDamageableComponent = null;
TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
await server.WaitPost(() =>
{
@@ -44,23 +41,24 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDestructionEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>();
sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
});
await server.WaitAssertion(() =>
{
var coordinates = sDestructibleEntity.Transform.Coordinates;
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute");
DamageSpecifier bruteDamage = new(bruteDamageGroup,50);
Assert.DoesNotThrow(() =>
{
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 50, true));
EntitySystem.Get<DamageableSystem>().TryChangeDamage(sDestructibleEntity.Uid, bruteDamage, true);
});
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
var threshold = sThresholdListenerComponent.ThresholdsReached[0].Threshold;
var threshold = sTestThresholdListenerSystem.ThresholdsReached[0].Threshold;
Assert.That(threshold.Triggered, Is.True);
Assert.That(threshold.Behaviors.Count, Is.EqualTo(3));

View File

@@ -27,21 +27,6 @@ namespace Content.IntegrationTests.Tests.Destructible
- 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:
@@ -56,43 +41,6 @@ namespace Content.IntegrationTests.Tests.Destructible
- 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}
@@ -102,7 +50,6 @@ namespace Content.IntegrationTests.Tests.Destructible
name: {DestructibleEntityId}
components:
- type: Damageable
damageContainer: TestMetallicDamageContainer
- type: Destructible
thresholds:
- trigger:
@@ -124,14 +71,12 @@ namespace Content.IntegrationTests.Tests.Destructible
max: 1
- !type:DoActsBehavior
acts: [""Breakage""]
- type: TestThresholdListener
- type: entity
id: {DestructibleDestructionEntityId}
name: {DestructibleDestructionEntityId}
components:
- type: Damageable
damageContainer: TestMetallicDamageContainer
- type: Destructible
thresholds:
- trigger:
@@ -148,14 +93,12 @@ namespace Content.IntegrationTests.Tests.Destructible
max: 1
- !type:DoActsBehavior # This must come last as it destroys the entity.
acts: [""Destruction""]
- type: TestThresholdListener
- type: entity
id: {DestructibleDamageTypeEntityId}
name: {DestructibleDamageTypeEntityId}
components:
- type: Damageable
damageContainer: TestMetallicDamageContainer
- type: Destructible
thresholds:
- trigger:
@@ -167,14 +110,12 @@ namespace Content.IntegrationTests.Tests.Destructible
- !type:DamageTypeTrigger
damageType: TestSlash
damage: 10
- type: TestThresholdListener
- type: entity
id: {DestructibleDamageGroupEntityId}
name: {DestructibleDamageGroupEntityId}
components:
- type: Damageable
damageContainer: TestMetallicDamageContainer
- type: Destructible
thresholds:
- trigger:
@@ -185,7 +126,6 @@ namespace Content.IntegrationTests.Tests.Destructible
damage: 10
- !type:DamageGroupTrigger
damageGroup: TestBurn
damage: 10
- type: TestThresholdListener";
damage: 10";
}
}

View File

@@ -5,7 +5,7 @@ using Content.Server.Destructible.Thresholds;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -17,7 +17,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{
[TestFixture]
[TestOf(typeof(DestructibleComponent))]
[TestOf(typeof(Threshold))]
[TestOf(typeof(DamageThreshold))]
public class DestructibleThresholdActivationTest : ContentIntegrationTest
{
[Test]
@@ -25,11 +25,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{
var server = StartServerDummyTicker(new ServerContentIntegrationOption
{
ExtraPrototypes = Prototypes,
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
ExtraPrototypes = Prototypes
});
await server.WaitIdleAsync();
@@ -37,11 +33,13 @@ namespace Content.IntegrationTests.Tests.Destructible
var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity;
IDamageableComponent sDamageableComponent = null;
IEntity sDestructibleEntity = null; ;
DamageableComponent sDamageableComponent = null;
DestructibleComponent sDestructibleComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null;
TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
DamageableSystem sDamageableSystem = null;
await server.WaitPost(() =>
{
@@ -50,34 +48,35 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>();
sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sDestructibleComponent = sDestructibleEntity.GetComponent<DestructibleComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>();
sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
});
await server.WaitRunTicks(5);
await server.WaitAssertion(() =>
{
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
});
await server.WaitAssertion(() =>
{
var bluntDamageType = sPrototypeManager.Index<DamageTypePrototype>("TestBlunt");
var bluntDamage = new DamageSpecifier(sPrototypeManager.Index<DamageTypePrototype>("TestBlunt"), 10);
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage, true);
// No thresholds reached yet, the earliest one is at 20 damage
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage, true);
// Only one threshold reached, 20
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
// Threshold 20
var msg = sThresholdListenerComponent.ThresholdsReached[0];
var msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var threshold = msg.Threshold;
// Check that it matches the YAML prototype
@@ -85,15 +84,15 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.NotNull(threshold.Trigger);
Assert.That(threshold.Triggered, Is.True);
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 30, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*3, 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));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
// Threshold 50
msg = sThresholdListenerComponent.ThresholdsReached[0];
msg = sTestThresholdListenerSystem.ThresholdsReached[0];
threshold = msg.Threshold;
// Check that it matches the YAML prototype
@@ -113,50 +112,50 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.NotNull(threshold.Trigger);
Assert.That(threshold.Triggered, Is.True);
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Damage for 50 again, up to 100 now
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 50, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*5, true);
// No thresholds reached as they weren't healed below the trigger amount
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached);
Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Set damage to 0
sDamageableComponent.TrySetAllDamage(0);
sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// Damage for 100, up to 100
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 100, true));
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*10, true);
// Two thresholds reached as damage increased past the previous, 20 and 50
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(2));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(2));
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal the entity for 40 damage, down to 60
sDamageableComponent.TryChangeDamage(bluntDamageType, -40, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*-4, true);
// Thresholds don't work backwards
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Damage for 10, up to 70
sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage, true);
// Not enough healing to de-trigger a threshold
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Heal by 30, down to 40
sDamageableComponent.TryChangeDamage(bluntDamageType, -30, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*-3, true);
// Thresholds don't work backwards
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Damage up to 50 again
sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage, true);
// The 50 threshold should have triggered again, after being healed
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1));
Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
msg = sThresholdListenerComponent.ThresholdsReached[0];
msg = sTestThresholdListenerSystem.ThresholdsReached[0];
threshold = msg.Threshold;
// Check that it matches the YAML prototype
@@ -178,22 +177,22 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(threshold.Triggered, Is.True);
// Reset thresholds reached
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal all damage
sDamageableComponent.TrySetAllDamage(0);
sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// Damage up to 50
sDamageableComponent.TryChangeDamage(bluntDamageType, 50, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*5, true);
// Check that the total damage matches
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// Both thresholds should have triggered
Assert.That(sThresholdListenerComponent.ThresholdsReached, Has.Exactly(2).Items);
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Has.Exactly(2).Items);
// Verify the first one, should be the lowest one (20)
msg = sThresholdListenerComponent.ThresholdsReached[0];
msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var trigger = (DamageTrigger) msg.Threshold.Trigger;
Assert.NotNull(trigger);
Assert.That(trigger.Damage, Is.EqualTo(20));
@@ -204,7 +203,7 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(threshold.Behaviors, Is.Empty);
// Verify the second one, should be the highest one (50)
msg = sThresholdListenerComponent.ThresholdsReached[1];
msg = sTestThresholdListenerSystem.ThresholdsReached[1];
trigger = (DamageTrigger) msg.Threshold.Trigger;
Assert.NotNull(trigger);
Assert.That(trigger.Damage, Is.EqualTo(50));
@@ -229,10 +228,10 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(threshold.Triggered, Is.True);
// Reset thresholds reached
sThresholdListenerComponent.ThresholdsReached.Clear();
sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal the entity completely
sDamageableComponent.TrySetAllDamage(0);
sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// Check that the entity has 0 damage
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
@@ -245,13 +244,13 @@ namespace Content.IntegrationTests.Tests.Destructible
}
// Damage the entity up to 50 damage again
sDamageableComponent.TryChangeDamage(bluntDamageType, 50, true);
sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*5, true);
// Check that the total damage matches
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// No thresholds should have triggered as they were already triggered before, and they are set to only trigger once
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Set both thresholds to trigger multiple times
foreach (var destructibleThreshold in sDestructibleComponent.Thresholds)
@@ -264,7 +263,7 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// They shouldn't have been triggered by changing TriggersOnce
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty);
Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
});
}
}

View File

@@ -0,0 +1,26 @@
using Content.Server.Destructible;
using Robust.Shared.GameObjects;
using System.Collections.Generic;
namespace Content.IntegrationTests.Tests.Destructible
{
/// <summary>
/// This is just a system for testing destructible thresholds. Whenever any threshold is reached, this will add that
/// threshold to a list for checking during testing.
/// </summary>
public class TestDestructibleListenerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DestructibleComponent, DamageThresholdReached>(AddThresholdsToList);
}
public void AddThresholdsToList(EntityUid _, DestructibleComponent comp, DamageThresholdReached args)
{
ThresholdsReached.Add(args);
}
public List<DamageThresholdReached> ThresholdsReached = new();
}
}

View File

@@ -1,25 +0,0 @@
using System.Collections.Generic;
using Content.Server.Destructible;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Destructible
{
public class TestThresholdListenerComponent : Component
{
public override string Name => "TestThresholdListener";
public List<DestructibleThresholdReachedMessage> ThresholdsReached { get; } = new();
public override void HandleMessage(ComponentMessage message, IComponent component)
{
base.HandleMessage(message, component);
switch (message)
{
case DestructibleThresholdReachedMessage msg:
ThresholdsReached.Add(msg);
break;
}
}
}
}

View File

@@ -1,15 +1,16 @@
#nullable enable
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Disposal.Tube.Components;
using Content.Server.Disposal.Unit.Components;
using Content.Server.Disposal.Unit.EntitySystems;
using Content.Server.Power.Components;
using Content.Shared.Coordinates;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Timing;
namespace Content.IntegrationTests.Tests.Disposal
{
@@ -19,13 +20,13 @@ namespace Content.IntegrationTests.Tests.Disposal
[TestOf(typeof(DisposalUnitComponent))]
public class DisposalUnitTest : ContentIntegrationTest
{
private void UnitInsert(DisposalUnitComponent unit, bool result, params IEntity[] entities)
private async Task UnitInsert(DisposalUnitComponent unit, bool result, params IEntity[] entities)
{
List<Task> insertionTasks = new();
foreach (var entity in entities)
{
var insertTask = unit.TryInsert(entity);
Assert.That(EntitySystem.Get<DisposalUnitSystem>().CanInsert(unit, entity), Is.EqualTo(result));
insertTask.ContinueWith(task =>
var insertTask = unit.TryInsert(entity).ContinueWith(task =>
{
Assert.That(task.Result, Is.EqualTo(result));
if (result)
@@ -34,7 +35,9 @@ namespace Content.IntegrationTests.Tests.Disposal
Assert.That(entity.Transform.Parent, Is.EqualTo(unit.Owner.Transform));
}
});
insertionTasks.Add(insertTask);
}
Task.WaitAll(insertionTasks.ToArray());
}
private void UnitContains(DisposalUnitComponent unit, bool result, params IEntity[] entities)
@@ -45,9 +48,9 @@ namespace Content.IntegrationTests.Tests.Disposal
}
}
private void UnitInsertContains(DisposalUnitComponent unit, bool result, params IEntity[] entities)
private async void UnitInsertContains(DisposalUnitComponent unit, bool result, params IEntity[] entities)
{
UnitInsert(unit, result, entities);
await UnitInsert(unit, result, entities);
UnitContains(unit, result, entities);
}
@@ -68,7 +71,9 @@ namespace Content.IntegrationTests.Tests.Disposal
- type: Body
- type: MobState
- type: Damageable
damagePrototype: biologicalDamageContainer
damageContainer: Biological
- type: Physics
bodyType: KinematicController
- type: entity
name: WrenchDummy
@@ -78,6 +83,8 @@ namespace Content.IntegrationTests.Tests.Disposal
- type: Tool
qualities:
- Anchoring
- type: Physics
bodyType: Dynamic
- type: entity
name: DisposalUnitDummy
@@ -94,44 +101,78 @@ namespace Content.IntegrationTests.Tests.Disposal
id: DisposalTrunkDummy
components:
- type: DisposalEntry
- type: Transform
anchored: true
";
[Test]
public async Task Test()
{
var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
var options = new ServerIntegrationOptions { ExtraPrototypes = Prototypes };
var server = StartServerDummyTicker(options);
await server.WaitIdleAsync();
IEntity human;
IEntity wrench;
DisposalUnitComponent unit;
IEntity human = default!;
IEntity wrench = default!;
IEntity disposalUnit = default!;
IEntity disposalTrunk = default!;
DisposalUnitComponent unit = default!;
EntityCoordinates coordinates = default!;
server.Assert(async () =>
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var pauseManager = server.ResolveDependency<IPauseManager>();
var componentFactory = server.ResolveDependency<IComponentFactory>();
var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
// Build up test environment
server.Post(() =>
{
var mapManager = IoCManager.Resolve<IMapManager>();
// Create a one tile grid to anchor our disposal unit to.
var mapId = mapManager.CreateMap();
mapManager.CreateNewMapEntity(MapId.Nullspace);
pauseManager.AddUninitializedMap(mapId);
var entityManager = IoCManager.Resolve<IEntityManager>();
var gridId = new GridId(1);
if (!mapManager.TryGetGrid(gridId, out var grid))
{
grid = mapManager.CreateGrid(mapId, gridId);
}
var tileDefinition = tileDefinitionManager["underplating"];
var tile = new Tile(tileDefinition.TileId);
coordinates = grid.ToCoordinates();
grid.SetTile(coordinates, tile);
pauseManager.DoMapInitialize(mapId);
});
await server.WaitAssertion(() =>
{
// Spawn the entities
human = entityManager.SpawnEntity("HumanDummy", MapCoordinates.Nullspace);
wrench = entityManager.SpawnEntity("WrenchDummy", MapCoordinates.Nullspace);
var disposalUnit = entityManager.SpawnEntity("DisposalUnitDummy", MapCoordinates.Nullspace);
var disposalTrunk = entityManager.SpawnEntity("DisposalTrunkDummy", disposalUnit.Transform.MapPosition);
human = entityManager.SpawnEntity("HumanDummy", coordinates);
wrench = entityManager.SpawnEntity("WrenchDummy", coordinates);
disposalUnit = entityManager.SpawnEntity("DisposalUnitDummy", coordinates);
disposalTrunk = entityManager.SpawnEntity("DisposalTrunkDummy", disposalUnit.Transform.MapPosition);
// Check that we have a grid, so that we can anchor our unit
Assert.That(mapManager.TryFindGridAt(disposalUnit.Transform.MapPosition, out var _));
// Test for components existing
Assert.True(disposalUnit.TryGetComponent(out unit!));
Assert.True(disposalTrunk.HasComponent<DisposalEntryComponent>());
// Can't insert, unanchored and unpowered
var physics = disposalUnit.GetComponent<IPhysBody>();
physics.BodyType = BodyType.Dynamic;
Assert.False(unit.Owner.Transform.Anchored);
unit.Owner.Transform.Anchored = false;
UnitInsertContains(unit, false, human, wrench, disposalUnit, disposalTrunk);
});
await server.WaitAssertion(() =>
{
// Anchor the disposal unit
physics.BodyType = BodyType.Static;
unit.Owner.Transform.Anchored = true;
// No power
Assert.False(unit.Powered);
@@ -141,19 +182,28 @@ namespace Content.IntegrationTests.Tests.Disposal
// Can insert mobs and items
UnitInsertContains(unit, true, human, wrench);
});
await server.WaitAssertion(() =>
{
// Move the disposal trunk away
disposalTrunk.Transform.WorldPosition += (1, 0);
// Fail to flush with a mob and an item
Flush(unit, false, human, wrench);
});
await server.WaitAssertion(() =>
{
// Move the disposal trunk back
disposalTrunk.Transform.WorldPosition -= (1, 0);
// Fail to flush with a mob and an item, no power
Flush(unit, false, human, wrench);
});
await server.WaitAssertion(() =>
{
// Remove power need
Assert.True(disposalUnit.TryGetComponent(out ApcPowerReceiverComponent? power));
power!.NeedsPower = false;
@@ -161,12 +211,13 @@ namespace Content.IntegrationTests.Tests.Disposal
// Flush with a mob and an item
Flush(unit, true, human, wrench);
});
await server.WaitAssertion(() =>
{
// Re-pressurizing
Flush(unit, false);
});
await server.WaitIdleAsync();
}
}
}

View File

@@ -16,7 +16,7 @@ namespace Content.Server.AI.Utility.Considerations.Combat.Melee
}
// Just went with max health
return meleeWeaponComponent.Damage / 300.0f;
return meleeWeaponComponent.Damage.Total / 300.0f;
}
}
}

View File

@@ -1,6 +1,6 @@
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
namespace Content.Server.AI.Utility.Considerations.Combat
{
@@ -10,7 +10,7 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || target.Deleted || !target.TryGetComponent(out IDamageableComponent? damageableComponent))
if (target == null || target.Deleted || !target.TryGetComponent(out DamageableComponent? damageableComponent))
{
return 0.0f;
}

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Server.AI.Components;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
@@ -32,7 +32,7 @@ namespace Content.Server.AI.WorldState.States.Mobs
continue;
}
if (player.AttachedEntity != Owner && player.AttachedEntity.HasComponent<IDamageableComponent>())
if (player.AttachedEntity != Owner && player.AttachedEntity.HasComponent<DamageableComponent>())
{
result.Add(player.AttachedEntity);
}

View File

@@ -5,11 +5,8 @@ using Content.Server.Pressure;
using Content.Shared.Alert;
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
@@ -22,21 +19,14 @@ 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";
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier Damage = default!;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(float airPressure)
{
if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) return;
if (!Owner.HasComponent<DamageableComponent>()) return;
var status = Owner.GetComponentOrNull<ServerAlertsComponent>();
var highPressureMultiplier = 1f;
@@ -59,7 +49,7 @@ namespace Content.Server.Atmos.Components
goto default;
// Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear.
damageable.TryChangeDamage(DamageType, Atmospherics.LowPressureDamage,true);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * Atmospherics.LowPressureDamage, true);
if (status == null) break;
@@ -79,10 +69,10 @@ namespace Content.Server.Atmos.Components
if(pressure < Atmospherics.WarningHighPressure)
goto default;
var damage = (int) MathF.Min((pressure / Atmospherics.HazardHighPressure) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);
var damageScale = (int) MathF.Min((pressure / Atmospherics.HazardHighPressure) * Atmospherics.PressureDamageCoefficient, Atmospherics.MaxHighPressureDamage);
// Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear.
damageable.TryChangeDamage(DamageType, damage,true);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * damageScale, true);
if (status == null) break;

View File

@@ -10,7 +10,6 @@ using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
using Content.Shared.Notification.Managers;
using Content.Shared.Temperature;
@@ -20,8 +19,6 @@ 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
{
@@ -45,17 +42,9 @@ 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"!;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier Damage = default!;
public void Extinguish()
{
@@ -102,12 +91,9 @@ namespace Content.Server.Atmos.Components
temp.ReceiveHeat(200 * FireStacks);
}
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{
// TODO ATMOS Fire resistance from armor
var damage = Math.Min((int) (FireStacks * 2.5f), 10);
damageable.TryChangeDamage(DamageType, damage, false);
}
var damageScale = Math.Min((int) (FireStacks * 2.5f), 10);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * damageScale);
AdjustFireStacks(-0.1f * (_resisting ? 10f : 1f));
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Server.Body.Circulatory;
using Content.Shared.Body.Components;
using Content.Shared.Body.Mechanism;

View File

@@ -12,9 +12,6 @@ using Content.Shared.Alert;
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;
@@ -92,21 +89,13 @@ 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"!;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier Damage = default!;
[DataField("damageRecovery", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier DamageRecovery = default!;
private Dictionary<Gas, float> NeedsAndDeficit(float frameTime)
{
@@ -358,27 +347,19 @@ namespace Content.Server.Body.Respiratory
alertsComponent.ShowAlert(AlertType.LowOxygen);
}
if (!Owner.TryGetComponent(out IDamageableComponent? damageable))
{
return;
}
damageable.TryChangeDamage(DamageType, _damage, false);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage);
}
private void StopSuffocation()
{
Suffocating = false;
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{
damageable.TryChangeDamage(DamageType, -_damageRecovery, false);
}
if (Owner.TryGetComponent(out ServerAlertsComponent? alertsComponent))
{
alertsComponent.ClearAlert(AlertType.LowOxygen);
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, DamageRecovery);
}
public GasMixture Clean(BloodstreamComponent bloodstream)

View File

@@ -2,13 +2,13 @@ using System;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Alert;
using Content.Server.Hands.Components;
using Content.Server.MobState.States;
using Content.Server.Pulling;
using Content.Server.Stunnable.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.Interaction.Helpers;
using Content.Shared.MobState.Components;
using Content.Shared.Notification.Managers;
using Content.Shared.Standing;
using Content.Shared.Verbs;

View File

@@ -8,7 +8,7 @@ using Content.Server.Items;
using Content.Server.Notification;
using Content.Server.Players;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Notification;
using Content.Shared.Notification.Managers;
using Robust.Server.Player;
@@ -30,13 +30,14 @@ namespace Content.Server.Chat.Commands
public string Help => Loc.GetString("suicide-command-help-text");
private void DealDamage(ISuicideAct suicide, IChatManager chat, IDamageableComponent damageableComponent, IEntity source, IEntity target)
private void DealDamage(ISuicideAct suicide, IChatManager chat, IEntity target)
{
var kind = suicide.Suicide(target, chat);
if (kind != SuicideKind.Special)
{
// TODO SUICIDE ..heh.. anyway, someone should fix this mess.
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
damageableComponent.TrySetDamage(kind switch
DamageSpecifier damage = new(kind switch
{
SuicideKind.Blunt => prototypeManager.Index<DamageTypePrototype>("Blunt"),
SuicideKind.Slash => prototypeManager.Index<DamageTypePrototype>("Slash"),
@@ -51,6 +52,7 @@ namespace Content.Server.Chat.Commands
_ => prototypeManager.Index<DamageTypePrototype>("Blunt")
},
200);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(target.Uid, damage, true);
}
}
@@ -77,7 +79,6 @@ namespace Content.Server.Chat.Commands
return;
}
var dmgComponent = owner.GetComponent<IDamageableComponent>();
//TODO: needs to check if the mob is actually alive
//TODO: maybe set a suicided flag to prevent resurrection?
@@ -90,7 +91,7 @@ namespace Content.Server.Chat.Commands
if (suicide != null)
{
DealDamage(suicide, chat, dmgComponent, itemComponent.Owner, owner);
DealDamage(suicide, chat, owner);
return;
}
}
@@ -106,7 +107,7 @@ namespace Content.Server.Chat.Commands
var suicide = entity.GetAllComponents<ISuicideAct>().FirstOrDefault();
if (suicide != null)
{
DealDamage(suicide, chat, dmgComponent, entity, owner);
DealDamage(suicide, chat, owner);
return;
}
}
@@ -119,7 +120,8 @@ namespace Content.Server.Chat.Commands
var selfMessage = Loc.GetString("suicide-command-default-text-self");
owner.PopupMessage(selfMessage);
dmgComponent.TrySetDamage(IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("Piercing"), 200);
DamageSpecifier damage = new(IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("Bloodloss"), 200);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(owner.Uid, damage, true);
// Prevent the player from returning to the body.
// Note that mind cannot be null because otherwise owner would be null.

View File

@@ -1,9 +1,9 @@
using Content.Server.Interaction.Components;
using Content.Server.MobState.States;
using Content.Server.Weapon.Melee;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.MobState.Components;
using Content.Shared.Notification.Managers;
using Content.Shared.Sound;
using Robust.Shared.Audio;

View File

@@ -3,66 +3,23 @@ using Content.Shared.Chemistry.Reagent;
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
{
/// <summary>
/// Default metabolism for medicine reagents. Attempts to find a DamageableComponent on the target,
/// and to update its damage values.
/// Default metabolism for medicine reagents.
/// </summary>
public class HealthChange : ReagentEffect, ISerializationHooks
public class HealthChange : ReagentEffect
{
/// <summary>
/// How much damage is changed when 1u of the reagent is metabolized.
/// Damage to apply every metabolism cycle. Damage Ignores resistances.
/// </summary>
[DataField("healthChange")]
public float AmountToChange { get; set; } = 1.0f;
[DataField("damage", required: true)]
public DamageSpecifier Damage = default!;
// TODO DAMAGE UNITS When damage units support decimals, get rid of this.
// See also _accumulatedDamage in ThirstComponent and HungerComponent
private float _accumulatedDamage;
/// <summary>
/// Damage group to change.
/// </summary>
// 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<IPrototypeManager>().Index<DamageGroupPrototype>(_damageGroupID);
}
/// <summary>
/// Changes damage if a DamageableComponent can be found.
/// </summary>
public override void Metabolize(IEntity solutionEntity, Solution.ReagentQuantity amount)
{
if (solutionEntity.TryGetComponent(out IDamageableComponent? damageComponent))
{
damageComponent.TryChangeDamage(DamageGroup, (int)AmountToChange, true);
float decHealthChange = (float) (AmountToChange - (int) AmountToChange);
_accumulatedDamage += decHealthChange;
if (_accumulatedDamage >= 1)
{
damageComponent.TryChangeDamage(DamageGroup, 1, true);
_accumulatedDamage -= 1;
}
else if(_accumulatedDamage <= -1)
{
damageComponent.TryChangeDamage(DamageGroup, -1, true);
_accumulatedDamage += 1;
}
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(solutionEntity.Uid, Damage, true);
}
}
}

View File

@@ -5,7 +5,7 @@ using System.Text;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
@@ -42,7 +42,7 @@ namespace Content.Server.Damage.Commands
return $"Damage Types:{msg}";
}
private delegate void Damage(IDamageableComponent damageable, bool ignoreResistances);
private delegate void Damage(IEntity entity, bool ignoreResistances);
private bool TryParseEntity(IConsoleShell shell, IPlayerSession? player, string arg,
[NotNullWhen(true)] out IEntity? entity)
@@ -85,7 +85,7 @@ namespace Content.Server.Damage.Commands
private bool TryParseDamageArgs(
IConsoleShell shell,
IPlayerSession? player,
IEntity target,
string[] args,
[NotNullWhen(true)] out Damage? func)
{
@@ -101,23 +101,12 @@ namespace Content.Server.Damage.Commands
if (_prototypeManager.TryIndex<DamageGroupPrototype>(args[0], out var damageGroup))
{
func = (damageable, ignoreResistances) =>
func = (entity, ignoreResistances) =>
{
if (!damageable.ApplicableDamageGroups.Contains(damageGroup))
{
shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} can not be damaged with damage group {damageGroup}");
var damage = new DamageSpecifier(damageGroup, amount);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(entity.Uid, damage, ignoreResistances);
return;
}
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} {damageGroup} damage{(ignoreResistances ? ", ignoring resistances." : ".")}");
shell.WriteLine($"Damaged entity {entity.Name} with id {entity.Uid} for {amount} {damageGroup} damage{(ignoreResistances ? ", ignoring resistances." : ".")}");
};
return true;
@@ -125,23 +114,12 @@ namespace Content.Server.Damage.Commands
// Fall back to DamageType
else if (_prototypeManager.TryIndex<DamageTypePrototype>(args[0], out var damageType))
{
func = (damageable, ignoreResistances) =>
func = (entity, ignoreResistances) =>
{
if (!damageable.IsSupportedDamageType(damageType))
{
shell.WriteLine($"Entity {damageable.Owner.Name} with id {damageable.Owner.Uid} can not be damaged with damage type {damageType}");
var damage = new DamageSpecifier(damageType, amount);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(entity.Uid, damage, ignoreResistances);
return;
}
if (!damageable.TryChangeDamage(damageType, 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} {damageType} damage{(ignoreResistances ? ", ignoring resistances." : ".")}");
shell.WriteLine($"Damaged entity {entity.Name} with id {entity.Uid} for {amount} {damageType} damage{(ignoreResistances ? ", ignoring resistances." : ".")}");
};
return true;
@@ -185,10 +163,6 @@ namespace Content.Server.Damage.Commands
shell.WriteLine($"Invalid number of arguments ({args.Length}).\n{Help}");
return;
case var n when n >= 2 && n <= 4:
if (!TryParseDamageArgs(shell, player, args, out damageFunc))
{
return;
}
var entityUid = n == 2 ? "_" : args[2];
@@ -199,6 +173,11 @@ namespace Content.Server.Damage.Commands
entity = parsedEntity;
if (!TryParseDamageArgs(shell, entity, args, out damageFunc))
{
return;
}
if (n == 4)
{
if (!bool.TryParse(args[3], out ignoreResistances))
@@ -218,13 +197,13 @@ namespace Content.Server.Damage.Commands
return;
}
if (!entity.TryGetComponent(out IDamageableComponent? damageable))
if (!entity.TryGetComponent(out DamageableComponent? damageable))
{
shell.WriteLine($"Entity {entity.Name} with id {entity.Uid} does not have a {nameof(IDamageableComponent)}.");
shell.WriteLine($"Entity {entity.Name} with id {entity.Uid} does not have a {nameof(DamageableComponent)}.");
return;
}
damageFunc(damageable, ignoreResistances);
damageFunc(entity, ignoreResistances);
}
}
}

View File

@@ -3,8 +3,6 @@ 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
@@ -19,8 +17,6 @@ namespace Content.Server.Damage.Components
[DataField("minimumSpeed")]
public float MinimumSpeed { get; set; } = 20f;
[DataField("baseDamage")]
public int BaseDamage { get; set; } = 5;
[DataField("factor")]
public float Factor { get; set; } = 1f;
[DataField("soundHit", required: true)]
@@ -36,16 +32,8 @@ namespace Content.Server.Damage.Components
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";
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier Damage = default!;
}
}

View File

@@ -10,19 +10,12 @@ namespace Content.Server.Damage.Components
{
public override string Name => "DamageOnLand";
[DataField("amount")]
[ViewVariables(VVAccess.ReadWrite)]
public int Amount = 1;
[DataField("ignoreResistances")]
[ViewVariables(VVAccess.ReadWrite)]
public 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")] public readonly string DamageTypeId = "Blunt";
public bool IgnoreResistances = false;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
public DamageSpecifier Damage = default!;
}
}

View File

@@ -2,13 +2,10 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Tools.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
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
@@ -19,28 +16,16 @@ namespace Content.Server.Damage.Components
public override string Name => "DamageOnToolInteract";
[DataField("damage")]
protected int Damage;
[DataField("tools")]
private List<ToolQuality> _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";
[DataField("weldingDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype WeldingDamageType = default!;
[DataField("defaultDamageType")]
private readonly string _defaultDamageTypeID = "Blunt";
public DamageSpecifier WeldingDamage = default!;
[DataField("defaultDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DefaultDamageType = default!;
protected override void Initialize()
{
base.Initialize();
WeldingDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_weldingDamageTypeID);
DefaultDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_defaultDamageTypeID);
}
public DamageSpecifier DefaultDamage = default!;
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
@@ -50,30 +35,22 @@ namespace Content.Server.Damage.Components
{
if (tool.HasQuality(ToolQuality.Welding) && toolQuality == ToolQuality.Welding)
{
if (eventArgs.Using.TryGetComponent(out WelderComponent? welder))
if (eventArgs.Using.TryGetComponent(out WelderComponent? welder) && welder.WelderLit)
{
if (welder.WelderLit) return CallDamage(eventArgs, tool);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(eventArgs.Target.Uid, WeldingDamage);
return true;
}
break; //If the tool quality is welding and its not lit or its not actually a welder that can be lit then its pointless to continue.
}
if (tool.HasQuality(toolQuality)) return CallDamage(eventArgs, tool);
}
}
return false;
}
protected bool CallDamage(InteractUsingEventArgs eventArgs, ToolComponent tool)
if (tool.HasQuality(toolQuality))
{
if (!eventArgs.Target.TryGetComponent<IDamageableComponent>(out var damageable))
return false;
damageable.TryChangeDamage(tool.HasQuality(ToolQuality.Welding)
? WeldingDamageType
: DefaultDamageType,
Damage);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(eventArgs.Target.Uid, DefaultDamage);
return true;
}
}
}
return false;
}
}
}

View File

@@ -3,8 +3,7 @@ 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;
using Robust.Shared.ViewVariables;
namespace Content.Server.Damage.Components
{
@@ -14,21 +13,13 @@ namespace Content.Server.Damage.Components
{
public override string Name => "DamageOtherOnHit";
[DataField("amount")]
public int Amount { get; } = 1;
[DataField("ignoreResistances")]
public bool IgnoreResistances { get; } = false;
[ViewVariables(VVAccess.ReadWrite)]
public bool IgnoreResistances = false;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
// 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<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
}
}

View File

@@ -2,7 +2,7 @@ using Content.Server.Atmos.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Server.Stunnable.Components;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.MobState;
using Content.Shared.Nutrition.Components;
using Content.Shared.Verbs;
@@ -34,7 +34,7 @@ namespace Content.Server.Damage
if (user.TryGetComponent<ActorComponent>(out var player))
{
if (!target.HasComponent<IDamageableComponent>() && !target.HasComponent<HungerComponent>() &&
if (!target.HasComponent<DamageableComponent>() && !target.HasComponent<HungerComponent>() &&
!target.HasComponent<ThirstComponent>())
{
return;
@@ -59,14 +59,9 @@ namespace Content.Server.Damage
public static void PerformRejuvenate(IEntity target)
{
if (target.TryGetComponent(out IDamageableComponent? damage))
if (target.TryGetComponent(out DamageableComponent? damageable))
{
damage.TrySetAllDamage(0);
}
if (target.TryGetComponent(out IMobStateComponent? mobState))
{
mobState.UpdateState(0);
EntitySystem.Get<DamageableSystem>().SetAllDamage(damageable, 0);
}
if (target.TryGetComponent(out HungerComponent? hunger))

View File

@@ -1,7 +1,7 @@
using Content.Server.Damage.Components;
using Content.Server.Stunnable.Components;
using Content.Shared.Audio;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
@@ -18,6 +18,7 @@ namespace Content.Server.Damage.Systems
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
@@ -27,7 +28,7 @@ namespace Content.Server.Damage.Systems
private void HandleCollide(EntityUid uid, DamageOnHighSpeedImpactComponent component, StartCollideEvent args)
{
if (!ComponentManager.TryGetComponent(uid, out IDamageableComponent? damageable)) return;
if (!ComponentManager.HasComponent<DamageableComponent>(uid)) return;
var otherBody = args.OtherFixture.Body.Owner;
var speed = args.OurFixture.Body.LinearVelocity.Length;
@@ -41,12 +42,11 @@ namespace Content.Server.Damage.Systems
component.LastHit = _gameTiming.CurTime;
var damage = (int) (component.BaseDamage * (speed / component.MinimumSpeed) * component.Factor);
if (ComponentManager.TryGetComponent(uid, out StunnableComponent? stun) && _robustRandom.Prob(component.StunChance))
stun.Stun(component.StunSeconds);
damageable.TryChangeDamage(component.DamageType, damage);
var damageScale = (speed / component.MinimumSpeed) * component.Factor;
_damageableSystem.TryChangeDamage(uid, component.Damage * damageScale);
}
}
}

View File

@@ -1,35 +1,24 @@
using Content.Server.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Throwing;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Content.Server.Damage.Systems
{
public sealed class DamageOnLandSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DamageOnLandComponent, ComponentInit>(HandleInit);
SubscribeLocalEvent<DamageOnLandComponent, LandEvent>(DamageOnLand);
}
private void HandleInit(EntityUid uid, DamageOnLandComponent component, ComponentInit args)
{
component.DamageType = _protoManager.Index<DamageTypePrototype>(component.DamageTypeId);
}
private void DamageOnLand(EntityUid uid, DamageOnLandComponent component, LandEvent args)
{
if (!ComponentManager.TryGetComponent<IDamageableComponent>(uid, out var damageable))
return;
damageable.TryChangeDamage(component.DamageType, component.Amount, component.IgnoreResistances);
_damageableSystem.TryChangeDamage(uid, component.Damage, component.IgnoreResistances);
}
}
}

View File

@@ -1,12 +1,15 @@
using Content.Server.Damage.Components;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.Throwing;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.Damage.Systems
{
public class DamageOtherOnHitSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<DamageOtherOnHitComponent, ThrowDoHitEvent>(OnDoHit);
@@ -14,10 +17,7 @@ namespace Content.Server.Damage.Systems
private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args)
{
if (!args.Target.TryGetComponent(out IDamageableComponent? damageable))
return;
damageable.TryChangeDamage(component.DamageType, component.Amount, component.IgnoreResistances);
_damageableSystem.TryChangeDamage(args.Target.Uid, component.Damage, component.IgnoreResistances);
}
}
}

View File

@@ -1,11 +1,10 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Server.Damage.Systems
{
@@ -13,6 +12,7 @@ namespace Content.Server.Damage.Systems
public class GodmodeSystem : EntitySystem
{
private readonly Dictionary<IEntity, OldEntityInformation> _entities = new();
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
@@ -40,11 +40,9 @@ namespace Content.Server.Damage.Systems
moved.Enabled = false;
}
if (entity.TryGetComponent(out IDamageableComponent? damageable))
if (entity.TryGetComponent(out DamageableComponent? damageable))
{
damageable.SupportedDamageTypes.Clear();
damageable.FullySupportedDamageGroups.Clear();
damageable.ApplicableDamageGroups.Clear();
_damageableSystem.SetDamage(damageable, new DamageSpecifier());
}
return true;
@@ -67,21 +65,11 @@ namespace Content.Server.Damage.Systems
moved.Enabled = old.MovedByPressure;
}
if (entity.TryGetComponent(out IDamageableComponent? damageable))
if (entity.TryGetComponent(out DamageableComponent? damageable))
{
if (old.SupportedDamageTypes != null)
if (old.Damage != null)
{
damageable.SupportedDamageTypes.UnionWith(old.SupportedDamageTypes);
}
if (old.SupportedDamageGroups != null)
{
damageable.FullySupportedDamageGroups.UnionWith(old.SupportedDamageGroups);
}
if (old.ApplicableDamageGroups != null)
{
damageable.ApplicableDamageGroups.UnionWith(old.ApplicableDamageGroups);
_damageableSystem.SetDamage(damageable, old.Damage);
}
}
@@ -114,11 +102,9 @@ namespace Content.Server.Damage.Systems
Entity = entity;
MovedByPressure = entity.IsMovedByPressure();
if (entity.TryGetComponent(out IDamageableComponent? damageable))
if (entity.TryGetComponent(out DamageableComponent? damageable))
{
SupportedDamageTypes = damageable.SupportedDamageTypes.ToHashSet();
SupportedDamageGroups = damageable.FullySupportedDamageGroups.ToHashSet();
ApplicableDamageGroups = damageable.ApplicableDamageGroups.ToHashSet();
Damage = damageable.Damage;
}
}
@@ -126,11 +112,7 @@ namespace Content.Server.Damage.Systems
public bool MovedByPressure { get; }
public HashSet<DamageTypePrototype>? SupportedDamageTypes { get; }
public HashSet<DamageGroupPrototype>? SupportedDamageGroups { get; }
public HashSet<DamageGroupPrototype>? ApplicableDamageGroups { get; }
public DamageSpecifier? Damage { get; }
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Server.Destructible.Thresholds;
using Content.Shared.Damage;
using Robust.Shared.GameObjects;
@@ -14,50 +14,11 @@ namespace Content.Server.Destructible
[RegisterComponent]
public class DestructibleComponent : Component
{
private DestructibleSystem _destructibleSystem = default!;
public override string Name => "Destructible";
[ViewVariables]
[DataField("thresholds")]
private List<Threshold> _thresholds = new();
public List<DamageThreshold> Thresholds = new();
public IReadOnlyList<Threshold> Thresholds => _thresholds;
protected override void Initialize()
{
base.Initialize();
_destructibleSystem = EntitySystem.Get<DestructibleSystem>();
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case DamageChangedMessage msg:
{
if (msg.Damageable.Owner != Owner)
{
break;
}
foreach (var threshold in _thresholds)
{
if (threshold.Reached(msg.Damageable, _destructibleSystem))
{
var thresholdMessage = new DestructibleThresholdReachedMessage(this, threshold);
SendMessage(thresholdMessage);
threshold.Execute(Owner, _destructibleSystem);
}
}
break;
}
}
}
}
}

View File

@@ -1,4 +1,6 @@
using Content.Shared.Acts;
using Content.Server.Destructible.Thresholds;
using Content.Shared.Acts;
using Content.Shared.Damage;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
@@ -13,5 +15,44 @@ namespace Content.Server.Destructible
[Dependency] public readonly IRobustRandom Random = default!;
[Dependency] public readonly AudioSystem AudioSystem = default!;
[Dependency] public readonly ActSystem ActSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DestructibleComponent, DamageChangedEvent>(Execute);
}
/// <summary>
/// Check if any thresholds were reached. if they were, execute them.
/// </summary>
public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
{
foreach (var threshold in component.Thresholds)
{
if (threshold.Reached(args.Damageable, this))
{
RaiseLocalEvent(uid, new DamageThresholdReached(component, threshold));
threshold.Execute(component.Owner, this);
}
}
}
}
// Currently only used for destructible integration tests. Unless other uses are found for this, maybe this should just be removed and the tests redone.
/// <summary>
/// Event raised when a <see cref="DamageThreshold"/> is reached.
/// </summary>
public class DamageThresholdReached : EntityEventArgs
{
public readonly DestructibleComponent Parent;
public readonly DamageThreshold Threshold;
public DamageThresholdReached(DestructibleComponent parent, DamageThreshold threshold)
{
Parent = parent;
Threshold = threshold;
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.Destructible.Thresholds;
using Robust.Shared.GameObjects;
namespace Content.Server.Destructible
{
public class DestructibleThresholdReachedMessage : ComponentMessage
{
public DestructibleThresholdReachedMessage(DestructibleComponent parent, Threshold threshold)
{
Parent = parent;
Threshold = threshold;
}
public DestructibleComponent Parent { get; }
public Threshold Threshold { get; }
}
}

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -9,7 +9,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Destructible.Thresholds
{
[DataDefinition]
public class Threshold
public class DamageThreshold
{
[DataField("behaviors")]
private List<IThresholdBehavior> _behaviors = new();
@@ -49,7 +49,7 @@ namespace Content.Server.Destructible.Thresholds
/// </summary>
[ViewVariables] public IReadOnlyList<IThresholdBehavior> Behaviors => _behaviors;
public bool Reached(IDamageableComponent damageable, DestructibleSystem system)
public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{
if (Trigger == null)
{

View File

@@ -1,6 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Destructible.Thresholds.Triggers
@@ -15,7 +15,7 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataField("triggers")]
public List<IThresholdTrigger> Triggers { get; set; } = new();
public bool Reached(IDamageableComponent damageable, DestructibleSystem system)
public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{
foreach (var trigger in Triggers)
{

View File

@@ -1,9 +1,8 @@
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;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Shared.Damage.Prototypes;
namespace Content.Server.Destructible.Thresholds.Triggers
{
@@ -15,12 +14,8 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[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<IPrototypeManager>().Index<DamageGroupPrototype>(_damageGroupID);
[DataField("damageGroup", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<DamageGroupPrototype>))]
public string DamageGroup { get; set; } = default!;
/// <summary>
/// The amount of damage at which this threshold will trigger.
@@ -28,15 +23,9 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataField("damage", required: true)]
public int Damage { get; set; } = default!;
public bool Reached(IDamageableComponent damageable, DestructibleSystem system)
public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{
if (DamageGroup == null)
{
return false;
}
return damageable.TryGetDamage(DamageGroup, out var damageReceived) &&
damageReceived >= Damage;
return damageable.DamagePerGroup[DamageGroup] >= Damage;
}
}
}

View File

@@ -1,5 +1,5 @@
using System;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Destructible.Thresholds.Triggers
@@ -18,7 +18,7 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataField("damage", required: true)]
public int Damage { get; set; } = default!;
public bool Reached(IDamageableComponent damageable, DestructibleSystem system)
public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{
return damageable.TotalDamage >= Damage;
}

View File

@@ -1,9 +1,8 @@
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;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Shared.Damage.Prototypes;
namespace Content.Server.Destructible.Thresholds.Triggers
{
@@ -15,24 +14,15 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataDefinition]
public class DamageTypeTrigger : 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("damageType", required:true)]
public string _damageTypeID { get; set; } = default!;
public DamageTypePrototype DamageType => IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
[DataField("damageType", required:true, customTypeSerializer: typeof(PrototypeIdSerializer<DamageTypePrototype>))]
public string DamageType { get; set; } = default!;
[DataField("damage", required: true)]
public int Damage { get; set; } = default!;
public bool Reached(IDamageableComponent damageable, DestructibleSystem system)
public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{
if (DamageType == null)
{
return false;
}
return damageable.TryGetDamage(DamageType, out var damageReceived) &&
return damageable.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) &&
damageReceived >= Damage;
}
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
namespace Content.Server.Destructible.Thresholds.Triggers
{
@@ -13,6 +13,6 @@ namespace Content.Server.Destructible.Thresholds.Triggers
/// dependencies from, if any.
/// </param>
/// <returns>true if this trigger has been reached, false otherwise.</returns>
bool Reached(IDamageableComponent damageable, DestructibleSystem system);
bool Reached(DamageableComponent damageable, DestructibleSystem system);
}
}

View File

@@ -1,6 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Destructible.Thresholds.Triggers
@@ -15,7 +15,7 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataField("triggers")]
public List<IThresholdTrigger> Triggers { get; } = new();
public bool Reached(IDamageableComponent damageable, DestructibleSystem system)
public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{
foreach (var trigger in Triggers)
{

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
@@ -40,35 +39,6 @@ namespace Content.Server.DoAfter
return new DoAfterComponentState(toAdd);
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case DamageChangedMessage msg:
if (DoAfters.Count == 0)
{
return;
}
if (!msg.TookDamage)
{
return;
}
foreach (var doAfter in _doAfters.Keys)
{
if (doAfter.EventArgs.BreakOnDamage)
{
doAfter.TookDamage = true;
}
}
break;
}
}
public void Add(DoAfter doAfter)
{
_doAfters.Add(doAfter, _runningIndex);

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Shared.Damage;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
@@ -14,6 +15,28 @@ namespace Content.Server.DoAfter
private readonly List<DoAfter> _cancelled = new();
private readonly List<DoAfter> _finished = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DoAfterComponent, DamageChangedEvent>(HandleDamage);
}
public void HandleDamage(EntityUid _, DoAfterComponent component, DamageChangedEvent args)
{
if (component.DoAfters.Count == 0 || !args.DamageIncreased)
{
return;
}
foreach (var doAfter in component.DoAfters)
{
if (doAfter.EventArgs.BreakOnDamage)
{
doAfter.TookDamage = true;
}
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);

View File

@@ -11,7 +11,6 @@ using Content.Server.Hands.Components;
using Content.Server.Stunnable.Components;
using Content.Server.Tools.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Content.Shared.Sound;
@@ -22,16 +21,11 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Broadphase;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
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
{
@@ -47,17 +41,9 @@ 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";
[DataField("crushDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier CrushDamage = default!;
public override DoorState State
{
@@ -90,7 +76,6 @@ namespace Content.Server.Doors.Components
private CancellationTokenSource? _stateChangeCancelTokenSource;
private CancellationTokenSource? _autoCloseCancelTokenSource;
private const int DoorCrushDamage = 15;
private const float DoorStunTime = 5f;
/// <summary>
@@ -537,7 +522,7 @@ namespace Content.Server.Doors.Components
foreach (var e in collidingentities)
{
if (!e.Owner.TryGetComponent(out StunnableComponent? stun)
|| !e.Owner.TryGetComponent(out IDamageableComponent? damage))
|| !e.Owner.HasComponent<DamageableComponent>())
{
continue;
}
@@ -550,7 +535,8 @@ namespace Content.Server.Doors.Components
hitsomebody = true;
CurrentlyCrushing.Add(e.Owner.Uid);
damage.TryChangeDamage(DamageType, DoorCrushDamage);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(e.Owner.Uid, CrushDamage);
stun.Paralyze(DoorStunTime);
}

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using Content.Server.Ghost.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Ghost;
using Content.Shared.MobState;
using Content.Shared.Preferences;
@@ -64,12 +64,10 @@ namespace Content.Server.GameTicking.Presets
{
canReturn = true;
if (playerEntity.TryGetComponent(out IDamageableComponent? damageable))
{
//todo: what if they dont breathe lol
//cry deeply
damageable.TrySetDamage(IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("Asphyxiation"), 200);
}
DamageSpecifier damage = new(IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("Asphyxiation"), 200);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(playerEntity.Uid, damage, true);
}
}

View File

@@ -15,7 +15,6 @@ using Content.Server.Traitor;
using Content.Server.TraitorDeathMatch.Components;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Inventory;
using Content.Shared.MobState;
using Content.Shared.PDA;
@@ -28,6 +27,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Random;
using Content.Shared.Damage.Prototypes;
namespace Content.Server.GameTicking.Presets
{
@@ -194,12 +194,9 @@ namespace Content.Server.GameTicking.Presets
{
if (mobState.IsCritical())
{
// 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.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100, true);
}
// TODO BODY SYSTEM KILL
var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(entity.Uid, damage, true);
}
else if (!mobState.IsDead())
{

View File

@@ -34,7 +34,7 @@ namespace Content.Server.GameTicking.Rules
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-death-match-added-announcement"));
_entityManager.EventBus.SubscribeEvent<DamageChangedEventArgs>(EventSource.Local, this, OnHealthChanged);
_entityManager.EventBus.SubscribeEvent<DamageChangedEvent>(EventSource.Local, this, OnHealthChanged);
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}
@@ -42,11 +42,11 @@ namespace Content.Server.GameTicking.Rules
{
base.Removed();
_entityManager.EventBus.UnsubscribeEvent<DamageChangedEventArgs>(EventSource.Local, this);
_entityManager.EventBus.UnsubscribeEvent<DamageChangedEvent>(EventSource.Local, this);
_playerManager.PlayerStatusChanged -= PlayerManagerOnPlayerStatusChanged;
}
private void OnHealthChanged(DamageChangedEventArgs message)
private void OnHealthChanged(DamageChangedEvent _)
{
_runDelayedCheck();
}

View File

@@ -6,7 +6,6 @@ using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
using Content.Shared.Light;
using Content.Shared.Notification.Managers;
@@ -16,7 +15,6 @@ 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.Maths;
using Robust.Shared.Player;
@@ -81,16 +79,13 @@ 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";
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
public DamageSpecifier Damage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
_lightBulbContainer = Owner.EnsureContainer<ContainerSlot>("light_bulb");
}
@@ -116,7 +111,7 @@ namespace Content.Server.Light.Components
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out IDamageableComponent? damageableComponent))
if (!eventArgs.User.HasComponent<DamageableComponent>())
{
Eject();
return false;
@@ -143,7 +138,7 @@ namespace Content.Server.Light.Components
void Burn()
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("powered-light-component-burn-hand"));
damageableComponent.TryChangeDamage(DamageType, 20);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(eventArgs.User.Uid, Damage);
SoundSystem.Play(Filter.Pvs(Owner), _burnHandSound.GetSound(), Owner);
}
@@ -285,17 +280,11 @@ namespace Content.Server.Light.Components
case PowerChangedMessage:
UpdateLight();
break;
case DamageChangedMessage msg:
TryDestroyBulb(msg);
break;
}
}
private void TryDestroyBulb(DamageChangedMessage msg)
public void TryDestroyBulb()
{
if (!msg.TookDamage)
return;
if (LightBulb == null || LightBulb.State == LightBulbState.Broken)
return;

View File

@@ -1,17 +1,19 @@
using System;
using Content.Server.Ghost;
using Content.Server.Light.Components;
using Content.Server.MachineLinking.Events;
using Content.Shared.Light;
using Content.Shared.Damage;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Content.Server.Light.Components;
using Content.Server.MachineLinking.Events;
using Robust.Shared.GameObjects;
namespace Content.Server.Light.EntitySystems
{
/// <summary>
/// System for the PoweredLightComponent. Currently bare-bones, to handle events from the DamageableSystem
/// </summary>
public class PoweredLightSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -21,6 +23,20 @@ namespace Content.Server.Light.EntitySystems
base.Initialize();
SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
SubscribeLocalEvent<PoweredLightComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<PoweredLightComponent, DamageChangedEvent>(HandleLightDamaged);
}
/// <summary>
/// Destroy the light bulb if the light took any damage.
/// </summary>
public void HandleLightDamaged(EntityUid uid, PoweredLightComponent component, DamageChangedEvent args)
{
// Was it being repaired, or did it take damage?
if (args.DamageIncreased)
{
// Eventually, this logic should all be done by this (or some other) system, not a component.
component.TryDestroyBulb();
}
}
private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)

View File

@@ -1,16 +1,12 @@
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.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
@@ -20,12 +16,9 @@ namespace Content.Server.Medical.Components
{
public override string Name => "Healing";
// 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 )]
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> Heal = new();
public DamageSpecifier Damage = default!;
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{
@@ -34,7 +27,7 @@ namespace Content.Server.Medical.Components
return false;
}
if (!eventArgs.Target.TryGetComponent(out IDamageableComponent? damageable))
if (!eventArgs.Target.HasComponent<DamageableComponent>())
{
return true;
}
@@ -55,10 +48,7 @@ namespace Content.Server.Medical.Components
return true;
}
foreach (var (damageTypeID, amount) in Heal)
{
damageable.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(damageTypeID), -amount, true);
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(eventArgs.Target.Uid, Damage, true);
return true;
}

View File

@@ -8,7 +8,6 @@ using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Acts;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.DragDrop;
using Content.Shared.Interaction;
using Content.Shared.MedicalScanner;
@@ -99,8 +98,7 @@ namespace Content.Server.Medical.Components
private static readonly MedicalScannerBoundUserInterfaceState EmptyUIState =
new(
null,
new Dictionary<string, int>(),
new Dictionary<string, int>(),
null,
false);
private MedicalScannerBoundUserInterfaceState GetUserInterfaceState()
@@ -116,18 +114,14 @@ namespace Content.Server.Medical.Components
return EmptyUIState;
}
if (!body.TryGetComponent(out IDamageableComponent? damageable))
if (!body.TryGetComponent(out DamageableComponent? damageable))
{
return EmptyUIState;
}
// Get dictionaries of damage, by fully supported damage groups and types
var groups = new Dictionary<string, int>(damageable.GetDamagePerFullySupportedGroupIDs);
var types = new Dictionary<string, int>(damageable.GetDamagePerTypeIDs);
if (_bodyContainer.ContainedEntity?.Uid == null)
{
return new MedicalScannerBoundUserInterfaceState(body.Uid, groups, types, true);
return new MedicalScannerBoundUserInterfaceState(body.Uid, damageable, true);
}
var cloningSystem = EntitySystem.Get<CloningSystem>();
@@ -135,7 +129,7 @@ namespace Content.Server.Medical.Components
mindComponent.Mind != null &&
cloningSystem.HasDnaScan(mindComponent.Mind);
return new MedicalScannerBoundUserInterfaceState(body.Uid, groups, types, scanned);
return new MedicalScannerBoundUserInterfaceState(body.Uid, damageable, scanned);
}
private void UpdateUserInterface()

View File

@@ -1,7 +1,6 @@
using System.Threading.Tasks;
using Content.Server.Weapon.Melee.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
using Content.Shared.Mining;
using Robust.Server.GameObjects;
@@ -24,16 +23,9 @@ 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<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(AsteroidRockVisuals.State, _random.Pick(SpriteStates));
@@ -46,7 +38,7 @@ namespace Content.Server.Mining.Components
if (!item.TryGetComponent(out MeleeWeaponComponent? meleeWeaponComponent))
return false;
Owner.GetComponent<IDamageableComponent>().TryChangeDamage(DamageType, meleeWeaponComponent.Damage);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, meleeWeaponComponent.Damage);
if (!item.TryGetComponent(out PickaxeComponent? pickaxeComponent))
return true;

View File

@@ -1,14 +0,0 @@
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
namespace Content.Server.MobState.States
{
[RegisterComponent]
[ComponentReference(typeof(SharedMobStateComponent))]
[ComponentReference(typeof(IMobStateComponent))]
public class MobStateComponent : SharedMobStateComponent
{
}
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Alert;
using Content.Server.Alert;
using Content.Shared.Alert;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.MobState;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
@@ -13,7 +13,7 @@ namespace Content.Server.MobState.States
{
base.UpdateState(entity, threshold);
if (!entity.TryGetComponent(out IDamageableComponent? damageable))
if (!entity.TryGetComponent(out DamageableComponent? damageable))
{
return;
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using Content.Server.Alert;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.MobState;
using Content.Shared.Movement.Components;
using Content.Shared.Nutrition.Components;
@@ -14,7 +13,6 @@ 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
{
@@ -23,9 +21,7 @@ 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;
private float _accumulatedFrameTime;
// Base stuff
[ViewVariables(VVAccess.ReadWrite)]
@@ -78,17 +74,9 @@ 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"!;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier Damage = default!;
public void HungerThresholdEffect(bool force = false)
{
@@ -196,21 +184,17 @@ namespace Content.Server.Nutrition.Components
return;
// --> Current Hunger is below dead threshold
if (!Owner.TryGetComponent(out IDamageableComponent? damageable))
return;
if (!Owner.TryGetComponent(out IMobStateComponent? mobState))
return;
if (!mobState.IsDead())
{
// --> 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);
_accumulatedFrameTime += frametime;
if (_accumulatedFrameTime >= 1)
{
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * (int) _accumulatedFrameTime, true);
_accumulatedFrameTime -= (int) _accumulatedFrameTime;
}
}
}

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using Content.Server.Alert;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.MobState;
using Content.Shared.Movement.Components;
using Content.Shared.Nutrition.Components;
@@ -14,7 +13,6 @@ 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
{
@@ -23,9 +21,7 @@ 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;
private float _accumulatedFrameTime;
// Base stuff
[ViewVariables(VVAccess.ReadWrite)]
@@ -77,17 +73,9 @@ 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";
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier Damage = default!;
public void ThirstThresholdEffect(bool force = false)
{
@@ -193,22 +181,17 @@ namespace Content.Server.Nutrition.Components
return;
// --> Current Hunger is below dead threshold
if (!Owner.TryGetComponent(out IDamageableComponent? damageable))
return;
if (!Owner.TryGetComponent(out IMobStateComponent? mobState))
return;
if (!mobState.IsDead())
{
// --> But they are not dead yet.
var damage = 2 * frametime;
_accumulatedDamage += damage - ((int) damage);
damageable.TryChangeDamage(DamageType, (int) damage);
if (_accumulatedDamage >= 1)
_accumulatedFrameTime += frametime;
if (_accumulatedFrameTime >= 1)
{
_accumulatedDamage -= 1;
damageable.TryChangeDamage(DamageType, 1, true);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * (int) _accumulatedFrameTime, true);
_accumulatedFrameTime -= (int) _accumulatedFrameTime;
}
}
}

View File

@@ -9,7 +9,6 @@ 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;
@@ -27,13 +26,14 @@ namespace Content.Server.Projectiles.Components
public override string Name => "Hitscan";
public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask;
[DataField("layers")] //todo WithFormat.Flags<CollisionLayer>()
private int _collisionMask = (int) CollisionGroup.Opaque;
[DataField("damage")]
public float Damage { get; set; } = 10f;
public float MaxLength => 20.0f;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
public float MaxLength => 20.0f;
private TimeSpan _startTime;
private TimeSpan _deathTime;
@@ -47,19 +47,6 @@ 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<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public void FireEffects(IEntity user, float distance, Angle angle, IEntity? hitEntity = null)
{
var effectSystem = EntitySystem.Get<EffectSystem>();

View File

@@ -13,12 +13,9 @@ namespace Content.Server.Projectiles.Components
[ComponentReference(typeof(SharedProjectileComponent))]
public class ProjectileComponent : SharedProjectileComponent
{
// 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")]
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> Damages { get; set; } = new();
public DamageSpecifier Damage = default!;
[DataField("deleteOnCollide")]
public bool DeleteOnCollide { get; } = true;

View File

@@ -1,22 +1,20 @@
using Content.Server.Camera;
using Content.Server.Projectiles.Components;
using Content.Shared.Body.Components;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
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!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
@@ -50,16 +48,12 @@ namespace Content.Server.Projectiles
SoundSystem.Play(playerFilter, soundHit, coordinates);
}
if (!otherEntity.Deleted && otherEntity.TryGetComponent(out IDamageableComponent? damage))
if (!otherEntity.Deleted)
{
EntityManager.TryGetEntity(component.Shooter, out var shooter);
foreach (var (damageTypeID, amount) in component.Damages)
{
damage.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(damageTypeID), amount);
}
_damageableSystem.TryChangeDamage(otherEntity.Uid, component.Damage);
component.DamagedEntity = true;
// "DamagedEntity" is misleading. Hit entity may be more accurate, as the damage may have been resisted
// by resistance sets.
}
// Damaging it can delete it

View File

@@ -1,50 +1,18 @@
using System.Threading.Tasks;
using Content.Server.Tools.Components;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction;
using Content.Shared.Notification;
using Content.Shared.Notification.Managers;
using Content.Shared.Tool;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Repairable
{
[RegisterComponent]
public class RepairableComponent : Component, IInteractUsing
public class RepairableComponent : Component
{
public override string Name => "Repairable";
[ViewVariables(VVAccess.ReadWrite)] [DataField("fuelCost")]
private int _fuelCost = 5;
public int FuelCost = 5;
[ViewVariables(VVAccess.ReadWrite)] [DataField("doAfterDelay")]
private int _doAfterDelay = 1;
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
// Only repair if you are using a lit welder
if (!eventArgs.Using.TryGetComponent(out WelderComponent? welder) || !welder.WelderLit)
return false;
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{
// Repair the target if it is damaged, oherwise do nothing
if (damageable.TotalDamage > 0)
{
if (!await welder.UseTool(eventArgs.User, Owner, _doAfterDelay, ToolQuality.Welding, _fuelCost))
return false;
damageable.TrySetAllDamage(0);
Owner.PopupMessage(eventArgs.User,
Loc.GetString("comp-repairable-repair",
("target", Owner),
("welder", eventArgs.Using)));
}
}
return true;
}
public int DoAfterDelay = 1;
}
}

View File

@@ -0,0 +1,46 @@
using Content.Server.Tools.Components;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Shared.Notification.Managers;
using Content.Shared.Tool;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Server.Repairable
{
public class ReairableSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<RepairableComponent, InteractUsingEvent>(Repair);
}
public async void Repair(EntityUid uid, RepairableComponent component, InteractUsingEvent args)
{
// Only repair if you are using a lit welder
if (!args.Used.TryGetComponent(out WelderComponent? welder) || !welder.WelderLit)
return;
// Only try repair the target if it is damaged
if (!component.Owner.TryGetComponent(out DamageableComponent? damageable) || damageable.TotalDamage == 0)
return;
// Can the welder actually repair this, does it have enough fuel?
if (!await welder.UseTool(args.User, component.Owner, component.DoAfterDelay, ToolQuality.Welding, component.FuelCost))
return;
// Repair all damage
_damageableSystem.SetAllDamage(damageable, 0);
component.Owner.PopupMessage(args.User,
Loc.GetString("comp-repairable-repair",
("target", component.Owner),
("welder", args.Used)));
args.Handled = true;
}
}
}

View File

@@ -3,7 +3,6 @@ using Content.Server.Alert;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -51,22 +50,13 @@ namespace Content.Server.Temperature.Components
}
}
// 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";
[DataField("coldDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype ColdDamageType = default!;
[DataField("hotDamageType")]
private readonly string _hotDamageTypeID = "Heat";
public DamageSpecifier ColdDamage = default!;
[DataField("heatDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype HotDamageType = default!;
protected override void Initialize()
{
base.Initialize();
ColdDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_coldDamageTypeID);
HotDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_hotDamageTypeID);
}
public DamageSpecifier HeatDamage = default!;
public void Update()
{
@@ -112,19 +102,18 @@ namespace Content.Server.Temperature.Components
}
}
if (!Owner.TryGetComponent(out IDamageableComponent? component)) return;
if (!Owner.HasComponent<DamageableComponent>()) return;
if (CurrentTemperature >= _heatDamageThreshold)
{
int tempDamage = (int) Math.Floor((CurrentTemperature - _heatDamageThreshold) * _tempDamageCoefficient);
component.TryChangeDamage(HotDamageType, tempDamage, false);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, HeatDamage * tempDamage);
}
else if (CurrentTemperature <= _coldDamageThreshold)
{
int tempDamage = (int) Math.Floor((_coldDamageThreshold - CurrentTemperature) * _tempDamageCoefficient);
component.TryChangeDamage(ColdDamageType, tempDamage, false);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, ColdDamage * tempDamage);
}
}
/// <summary>

View File

@@ -4,8 +4,6 @@ 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
{
@@ -46,10 +44,6 @@ namespace Content.Server.Weapon.Melee.Components
[DataField("range")]
public float Range { get; set; } = 1;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("damage")]
public int Damage { get; set; } = 5;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clickAttackEffect")]
public bool ClickAttackEffect { get; set; } = true;
@@ -57,16 +51,8 @@ namespace Content.Server.Weapon.Melee.Components
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";
[DataField("damage", required:true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public DamageSpecifier Damage = default!;
}
}

View File

@@ -6,7 +6,7 @@ using Content.Server.Chemistry.Components;
using Content.Server.Cooldown;
using Content.Server.Weapon.Melee.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Physics;
@@ -25,6 +25,7 @@ namespace Content.Server.Weapon.Melee
public sealed class MeleeWeaponSystem : EntitySystem
{
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private SolutionContainerSystem _solutionsSystem = default!;
public override void Initialize()
@@ -87,12 +88,7 @@ namespace Content.Server.Weapon.Melee
{
var targets = new[] { target };
SendAnimation(comp.ClickArc, angle, args.User, owner, targets, comp.ClickAttackEffect, false);
if (target.TryGetComponent(out IDamageableComponent? damageableComponent))
{
damageableComponent.TryChangeDamage(comp.DamageType, comp.Damage);
}
_damageableSystem.TryChangeDamage(target.Uid, comp.Damage);
SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), target);
}
}
@@ -133,7 +129,7 @@ namespace Content.Server.Weapon.Melee
if (!entity.Transform.IsMapTransform || entity == args.User)
continue;
if (ComponentManager.HasComponent<IDamageableComponent>(entity.Uid))
if (ComponentManager.HasComponent<DamageableComponent>(entity.Uid))
{
hitEntities.Add(entity);
}
@@ -157,10 +153,7 @@ namespace Content.Server.Weapon.Melee
foreach (var entity in hitEntities)
{
if (entity.TryGetComponent<IDamageableComponent>(out var damageComponent))
{
damageComponent.TryChangeDamage(comp.DamageType, comp.Damage);
}
_damageableSystem.TryChangeDamage(entity.Uid, comp.Damage);
}
}

View File

@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Power.Components;
using Content.Server.Projectiles.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Sound;
@@ -188,13 +186,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
{
if (energyRatio < 1.0)
{
var newDamages = new Dictionary<string, int>(projectileComponent.Damages.Count);
foreach (var (damageType, damage) in projectileComponent.Damages)
{
newDamages.Add(damageType, (int) (damage * energyRatio));
}
projectileComponent.Damages = newDamages;
projectileComponent.Damage *= energyRatio;
}
} else if (entity.TryGetComponent(out HitscanComponent? hitscanComponent))
{

View File

@@ -6,7 +6,7 @@ using Content.Server.Camera;
using Content.Server.Projectiles.Components;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Audio;
using Content.Shared.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Sound;
@@ -395,13 +395,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
var result = rayCastResults[0];
var distance = result.Distance;
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
if (!result.HitEntity.TryGetComponent(out IDamageableComponent? damageable))
return;
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
EntitySystem.Get<DamageableSystem>().TryChangeDamage(result.HitEntity.Uid, hitscan.Damage);
}
else
{

View File

@@ -1,5 +1,4 @@
using System;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.CombatMode;
using Content.Server.Hands.Components;
@@ -8,9 +7,7 @@ using Content.Server.Stunnable.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Hands;
using Content.Shared.Interaction.Events;
using Content.Shared.Notification.Managers;
using Content.Shared.Sound;
using Content.Shared.Weapons.Ranged.Components;
@@ -27,8 +24,6 @@ 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
{
@@ -58,16 +53,9 @@ 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<string, int> ClumsyDamage { get; set; } = new()
{
{ "Blunt", 10 },
{ "Heat", 5 }
};
public DamageSpecifier? ClumsyDamage;
public Func<bool>? WeaponCanFireHandler;
public Func<IEntity, bool>? UserCanFireHandler;
@@ -179,16 +167,10 @@ namespace Content.Server.Weapon.Ranged
_lastFireTime = curTime;
if (ClumsyCheck && ClumsyComponent.TryRollClumsy(user, ClumsyExplodeChance))
if (ClumsyCheck && ClumsyDamage != null && ClumsyComponent.TryRollClumsy(user, ClumsyExplodeChance))
{
//Wound them
if (user.TryGetComponent(out IDamageableComponent? health))
{
foreach (KeyValuePair<string, int> damage in ClumsyDamage)
{
health.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(damage.Key), damage.Value);
}
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(user.Uid, ClumsyDamage);
// Knock them down
if (user.TryGetComponent(out StunnableComponent? stun))

View File

@@ -4,7 +4,6 @@ using Content.Server.Destructible.Thresholds.Triggers;
using Content.Server.Notification;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Rounding;
@@ -41,22 +40,7 @@ namespace Content.Server.Window
[DataField("knockSound")]
private SoundSpecifier _knockSound = new SoundPathSpecifier("/Audio/Effects/glass_knock.ogg");
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case DamageChangedMessage msg:
{
var current = msg.Damageable.TotalDamage;
UpdateVisuals(current);
break;
}
}
}
private void UpdateVisuals(int currentDamage)
public void UpdateVisuals(int currentDamage)
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
Owner.TryGetComponent(out DestructibleComponent? destructible))
@@ -75,7 +59,7 @@ namespace Content.Server.Window
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
if (!Owner.TryGetComponent(out IDamageableComponent? damageable) ||
if (!Owner.TryGetComponent(out DamageableComponent? damageable) ||
!Owner.TryGetComponent(out DestructibleComponent? destructible))
{
return;

View File

@@ -0,0 +1,19 @@
using Content.Shared.Damage;
using Robust.Shared.GameObjects;
namespace Content.Server.Window
{
public class WindowSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WindowComponent, DamageChangedEvent>(UpdateVisuals);
}
public void UpdateVisuals(EntityUid _, WindowComponent component, DamageChangedEvent args)
{
component.UpdateVisuals(args.Damageable.TotalDamage);
}
}
}

View File

@@ -9,7 +9,7 @@ using Content.Shared.Body.Preset;
using Content.Shared.Body.Slot;
using Content.Shared.Body.Template;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Movement.Components;
using Content.Shared.Standing;
using Robust.Shared.GameObjects;
@@ -73,23 +73,6 @@ namespace Content.Shared.Body.Components
public SharedBodyPartComponent? CenterPart => CenterSlot?.Part;
/// <summary>
/// Amount of damage to deal when all vital organs are removed.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("vitalPartsRemovedDamage")]
public int VitalPartsRemovedDamage { get; set; } = 300!;
/// <summary>
/// Damage type to deal when all vital organs are removed.
/// </summary>
// 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();
@@ -98,7 +81,6 @@ namespace Content.Shared.Body.Components
// TODO BODY Move to template or somewhere else
if (TemplateId != null)
{
VitalPartsRemovedDamageType = _prototypeManager.Index<DamageTypePrototype>(_vitalPartsRemovedDamageTypeID);
var template = _prototypeManager.Index<BodyTemplatePrototype>(TemplateId);
foreach (var (id, partType) in template.Slots)
@@ -207,13 +189,11 @@ namespace Content.Shared.Body.Components
EntitySystem.Get<StandingStateSystem>().Down(Owner);
}
// creadth: immediately kill entity if last vital part removed
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{
if (part.IsVital && SlotParts.Count(x => x.Value.PartType == part.PartType) == 0)
{
damageable.TryChangeDamage(VitalPartsRemovedDamageType, VitalPartsRemovedDamage, true); // TODO BODY KILL
}
// TODO BODY SYSTEM KILL : Find a more elegant way of killing em than just dumping bloodloss damage.
var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Bloodloss"), 300);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(part.Owner.Uid, damage);
}
OnBodyChanged();
@@ -490,6 +470,7 @@ namespace Content.Shared.Body.Components
}
}
private void OnBodyChanged()
{
Dirty();

View File

@@ -1,499 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Acts;
using Content.Shared.Damage.Container;
using Content.Shared.Damage.Resistances;
using Content.Shared.Radiation;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Components
{
/// <summary>
/// Component that allows attached entities to take damage.
/// </summary>
/// <remarks>
/// The supported damage types are specified using a <see cref="DamageContainerPrototype"/>s. DamageContainers
/// are effectively a dictionary of damage types and damage numbers, along with functions to modify them. Damage
/// groups are collections of damage types. A damage group is 'applicable' to a damageable component if it
/// supports at least one damage type in that group. A subset of these groups may be 'fully supported' when every
/// member of the group is supported by the container. This basic version never dies (thus can take an
/// indefinite amount of damage).
/// </remarks>
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
[NetworkedComponent()]
public class DamageableComponent : Component, IDamageableComponent, IRadiationAct, ISerializationHooks
{
public override string Name => "Damageable";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
/// <summary>
/// The main damage dictionary. All the damage information is stored in this dictionary with <see cref="DamageTypePrototype"/> keys.
/// </summary>
private Dictionary<DamageTypePrototype, int> _damageDict = new();
[DataField("resistances")]
public string ResistanceSetId { get; set; } = "defaultResistances";
[ViewVariables] public ResistanceSet Resistances { get; set; } = new();
// TODO DAMAGE Use as default values, specify overrides in a separate property through yaml for better (de)serialization
[ViewVariables]
[DataField("damageContainer")]
public string DamageContainerId { get; set; } = "metallicDamageContainer";
// TODO DAMAGE Cache this
// When moving logic from damageableComponent --> Damage System, make damageSystem update these on damage change.
[ViewVariables] public int TotalDamage => _damageDict.Values.Sum();
[ViewVariables] public IReadOnlyDictionary<DamageTypePrototype, int> GetDamagePerType => _damageDict;
[ViewVariables] public IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerApplicableGroup => DamageTypeDictToDamageGroupDict(_damageDict, ApplicableDamageGroups);
[ViewVariables] public IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerFullySupportedGroup => DamageTypeDictToDamageGroupDict(_damageDict, FullySupportedDamageGroups);
// Whenever sending over network, also need a <string, int> dictionary
// TODO DAMAGE MAYBE Cache this?
public IReadOnlyDictionary<string, int> GetDamagePerApplicableGroupIDs => ConvertDictKeysToIDs(GetDamagePerApplicableGroup);
public IReadOnlyDictionary<string, int> GetDamagePerFullySupportedGroupIDs => ConvertDictKeysToIDs(GetDamagePerFullySupportedGroup);
public IReadOnlyDictionary<string, int> GetDamagePerTypeIDs => ConvertDictKeysToIDs(_damageDict);
// TODO PROTOTYPE Replace these datafield variables with prototype references, once they are supported.
// Also requires appropriate changes in OnExplosion() and RadiationAct()
[ViewVariables]
[DataField("radiationDamageTypes")]
public List<string> RadiationDamageTypeIDs { get; set; } = new() {"Radiation"};
[ViewVariables]
[DataField("explosionDamageTypes")]
public List<string> ExplosionDamageTypeIDs { get; set; } = new() { "Piercing", "Heat" };
public HashSet<DamageGroupPrototype> ApplicableDamageGroups { get; } = new();
public HashSet<DamageGroupPrototype> FullySupportedDamageGroups { get; } = new();
public HashSet<DamageTypePrototype> SupportedDamageTypes { get; } = new();
protected override void Initialize()
{
base.Initialize();
// TODO DAMAGE Serialize damage done and resistance changes
var damageContainerPrototype = _prototypeManager.Index<DamageContainerPrototype>(DamageContainerId);
ApplicableDamageGroups.Clear();
FullySupportedDamageGroups.Clear();
SupportedDamageTypes.Clear();
//Get Damage groups/types from the DamageContainerPrototype.
DamageContainerId = damageContainerPrototype.ID;
ApplicableDamageGroups.UnionWith(damageContainerPrototype.ApplicableDamageGroups);
FullySupportedDamageGroups.UnionWith(damageContainerPrototype.FullySupportedDamageGroups);
SupportedDamageTypes.UnionWith(damageContainerPrototype.SupportedDamageTypes);
//initialize damage dictionary 0 damage
_damageDict = new(SupportedDamageTypes.Count);
foreach (var type in SupportedDamageTypes)
{
_damageDict.Add(type, 0);
}
Resistances = new ResistanceSet(_prototypeManager.Index<ResistanceSetPrototype>(ResistanceSetId));
}
protected override void Startup()
{
base.Startup();
ForceHealthChangedEvent();
}
public override ComponentState GetComponentState(ICommonSession player)
{
return new DamageableComponentState(GetDamagePerTypeIDs);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is DamageableComponentState state))
{
return;
}
_damageDict.Clear();
foreach (var (type, damage) in state.DamageDict)
{
_damageDict[_prototypeManager.Index<DamageTypePrototype>(type)] = damage;
}
}
public int GetDamage(DamageTypePrototype type)
{
return GetDamagePerType.GetValueOrDefault(type);
}
public bool TryGetDamage(DamageTypePrototype type, out int damage)
{
return GetDamagePerType.TryGetValue(type, out damage);
}
public int GetDamage(DamageGroupPrototype group)
{
return GetDamagePerApplicableGroup.GetValueOrDefault(group);
}
public bool TryGetDamage(DamageGroupPrototype group, out int damage)
{
return GetDamagePerApplicableGroup.TryGetValue(group, out damage);
}
public bool IsApplicableDamageGroup(DamageGroupPrototype group)
{
return ApplicableDamageGroups.Contains(group);
}
public bool IsFullySupportedDamageGroup(DamageGroupPrototype group)
{
return FullySupportedDamageGroups.Contains(group);
}
public bool IsSupportedDamageType(DamageTypePrototype type)
{
return SupportedDamageTypes.Contains(type);
}
public bool TrySetDamage(DamageGroupPrototype group, int newValue)
{
if (!ApplicableDamageGroups.Contains(group))
{
return false;
}
if (newValue < 0)
{
// invalid value
return false;
}
foreach (var type in group.DamageTypes)
{
TrySetDamage(type, newValue);
}
return true;
}
public bool TrySetAllDamage(int newValue)
{
if (newValue < 0)
{
// invalid value
return false;
}
foreach (var type in SupportedDamageTypes)
{
TrySetDamage(type, newValue);
}
return true;
}
public bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false)
{
// Check if damage type is supported, and get the current value if it is.
if (!GetDamagePerType.TryGetValue(type, out var current))
{
return false;
}
if (amount == 0)
{
return false;
}
// Apply resistances (does nothing if amount<0)
var finalDamage = amount;
if (!ignoreDamageResistances)
{
finalDamage = Resistances.CalculateDamage(type, amount);
}
if (finalDamage == 0)
return false;
// Are we healing below zero?
if (current + finalDamage < 0)
{
if (current == 0)
// Damage type is supported, but there is nothing to do
return false;
// Cap healing down to zero
_damageDict[type] = 0;
finalDamage = -current;
}
else
{
_damageDict[type] = current + finalDamage;
}
current = _damageDict[type];
var datum = new DamageChangeData(type, current, finalDamage);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
public bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false)
{
var types = group.DamageTypes.ToArray();
if (amount < 0)
{
// We are Healing. Keep track of how much we can hand out (with a better var name for readability).
var availableHealing = -amount;
// Get total group damage.
var damageToHeal = GetDamagePerApplicableGroup[group];
// Is there any damage to even heal?
if (damageToHeal == 0)
return false;
// If total healing is more than there is damage, just set to 0 and return.
if (damageToHeal <= availableHealing)
{
TrySetDamage(group, 0);
return true;
}
// Partially heal each damage group
int healing, damage;
foreach (var type in types)
{
if (!_damageDict.TryGetValue(type, out damage))
{
// Damage Type is not supported. Continue without reducing availableHealing
continue;
}
// Apply healing to the damage type. The healing amount may be zero if either damage==0, or if
// integer rounding made it zero (i.e., damage is small)
healing = (availableHealing * damage) / damageToHeal;
TryChangeDamage(type, -healing, ignoreDamageResistances);
// remove this damage type from the damage we consider for future loops, regardless of how much we
// actually healed this type.
damageToHeal -= damage;
availableHealing -= healing;
// If we now healed all the damage, exit. otherwise 1/0 and universe explodes.
if (damageToHeal == 0)
{
break;
}
}
// Damage type is supported, there was damage to heal, and resistances were ignored
// --> Damage must have changed
return true;
}
else if (amount > 0)
{
// Resistances may result in no actual damage change. We need to keep track if any damage got through.
var damageChanged = false;
// We are adding damage. Keep track of how much we can dish out (with a better var name for readability).
var availableDamage = amount;
// How many damage types do we have to distribute over?.
var numberDamageTypes = types.Length;
// Apply damage to each damage group
int damage;
foreach (var type in types)
{
// Distribute the remaining damage over the remaining damage types.
damage = availableDamage / numberDamageTypes;
// Try apply the damage type. If damage type is not supported, this has no effect.
// We also use the return value to check whether any damage has changed
damageChanged = TryChangeDamage(type, damage, ignoreDamageResistances) || damageChanged;
// regardless of whether we dealt damage, reduce the amount to distribute.
availableDamage -= damage;
numberDamageTypes -= 1;
}
return damageChanged;
}
// amount==0 no damage change.
return false;
}
public bool TrySetDamage(DamageTypePrototype type, int newValue)
{
if (!_damageDict.TryGetValue(type, out var oldValue))
{
return false;
}
if (newValue < 0)
{
// invalid value
return false;
}
if (oldValue == newValue)
{
// No health change.
// But we are trying to set, not trying to change.
return true;
}
_damageDict[type] = newValue;
var delta = newValue - oldValue;
var datum = new DamageChangeData(type, 0, delta);
var data = new List<DamageChangeData> {datum};
OnHealthChanged(data);
return true;
}
public void ForceHealthChangedEvent()
{
var data = new List<DamageChangeData>();
foreach (var type in SupportedDamageTypes)
{
var damage = GetDamage(type);
var datum = new DamageChangeData(type, damage, 0);
data.Add(datum);
}
OnHealthChanged(data);
}
private void OnHealthChanged(List<DamageChangeData> changes)
{
var args = new DamageChangedEventArgs(this, changes);
OnHealthChanged(args);
}
protected virtual void OnHealthChanged(DamageChangedEventArgs e)
{
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, e);
var message = new DamageChangedMessage(this, e.Data);
SendMessage(message);
Dirty();
}
public void RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
{
var totalDamage = Math.Max((int)(frameTime * radiation.RadsPerSecond), 1);
foreach (var typeID in RadiationDamageTypeIDs)
{
TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(typeID), totalDamage);
}
}
public void OnExplosion(ExplosionEventArgs eventArgs)
{
var damage = eventArgs.Severity switch
{
ExplosionSeverity.Light => 20,
ExplosionSeverity.Heavy => 60,
ExplosionSeverity.Destruction => 250,
_ => throw new ArgumentOutOfRangeException()
};
foreach (var typeID in ExplosionDamageTypeIDs)
{
TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(typeID), damage);
}
}
/// <summary>
/// Take a dictionary with <see cref="IPrototype"/> keys and return a dictionary using <see cref="IPrototype.ID"/> as keys
/// instead.
/// </summary>
/// <remarks>
/// Useful when sending damage type and group prototypes dictionaries over the network.
/// </remarks>
public static IReadOnlyDictionary<string, int>
ConvertDictKeysToIDs<TPrototype>(IReadOnlyDictionary<TPrototype, int> prototypeDict)
where TPrototype : IPrototype
{
Dictionary<string, int> idDict = new(prototypeDict.Count);
foreach (var entry in prototypeDict)
{
idDict.Add(entry.Key.ID, entry.Value);
}
return idDict;
}
/// <summary>
/// Convert a dictionary with damage type keys to a dictionary of damage groups keys.
/// </summary>
/// <remarks>
/// Takes a dictionary with damage types as keys and integers as values, and an iterable list of damage
/// groups. Returns a dictionary with damage group keys, with values calculated by adding up the values for
/// each damage type in that group. If a damage type is associated with more than one supported damage
/// group, it will contribute to the total of each group. Conversely, some damage types may not contribute
/// to the new dictionary if their associated group(s) are not in given list of groups.
/// </remarks>
public static IReadOnlyDictionary<DamageGroupPrototype, int>
DamageTypeDictToDamageGroupDict(IReadOnlyDictionary<DamageTypePrototype, int> damageTypeDict, IEnumerable<DamageGroupPrototype> groupKeys)
{
var damageGroupDict = new Dictionary<DamageGroupPrototype, int>();
int damageGroupSumDamage, damageTypeDamage;
// iterate over the list of group keys for our new dictionary
foreach (var group in groupKeys)
{
// For each damage type in this group, add up the damage present in the given dictionary
damageGroupSumDamage = 0;
foreach (var type in group.DamageTypes)
{
// if the damage type is in the dictionary, add it's damage to the group total.
if (damageTypeDict.TryGetValue(type, out damageTypeDamage))
{
damageGroupSumDamage += damageTypeDamage;
}
}
damageGroupDict.Add(group, damageGroupSumDamage);
}
return damageGroupDict;
}
}
[Serializable, NetSerializable]
public class DamageableComponentState : ComponentState
{
public readonly IReadOnlyDictionary<string, int> DamageDict;
public DamageableComponentState(IReadOnlyDictionary<string, int> damageDict)
{
DamageDict = damageDict;
}
}
}

View File

@@ -1,225 +0,0 @@
using System.Collections.Generic;
using Content.Shared.Acts;
using Content.Shared.Damage.Resistances;
using Robust.Shared.GameObjects;
namespace Content.Shared.Damage.Components
{
public interface IDamageableComponent : IComponent, IExAct
{
/// <summary>
/// The sum of all damages types in the DamageableComponent.
/// </summary>
int TotalDamage { get; }
/// <summary>
/// Returns a dictionary of the damage in the container, indexed by applicable <see cref="DamageGroupPrototype"/>.
/// </summary>
/// <remarks>
/// The values represent the sum of all damage in each group. If a supported damage type is a member of more than one group, it will contribute to each one.
/// Therefore, the sum of the values may be greater than the sum of the values in the dictionary returned by <see cref="GetDamagePerType"/>
/// </remarks>
IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerApplicableGroup { get; }
/// <summary>
/// Returns a dictionary of the damage in the container, indexed by fully supported instances of <see
/// cref="DamageGroupPrototype"/>.
/// </summary>
/// <remarks>
/// The values represent the sum of all damage in each group. As the damage container may have some damage
/// types that are not part of a fully supported damage group, the sum of the values may be less of the values
/// in the dictionary returned by <see cref="GetDamagePerType"/>. On the other hand, if a supported damage type
/// is a member of more than one group, it will contribute to each one. Therefore, the sum may also be greater
/// instead.
/// </remarks>
IReadOnlyDictionary<DamageGroupPrototype, int> GetDamagePerFullySupportedGroup { get; }
/// <summary>
/// Returns a dictionary of the damage in the container, indexed by <see cref="DamageTypePrototype"/>.
/// </summary>
IReadOnlyDictionary<DamageTypePrototype, int> GetDamagePerType { get; }
/// <summary>
/// Like <see cref="GetDamagePerApplicableGroup"/>, but indexed by <see cref="DamageGroupPrototype.ID"/>
/// </summary>
IReadOnlyDictionary<string, int> GetDamagePerApplicableGroupIDs { get; }
/// <summary>
/// Like <see cref="GetDamagePerFullySupportedGroup"/>, but indexed by <see cref="DamageGroupPrototype.ID"/>
/// </summary>
IReadOnlyDictionary<string, int> GetDamagePerFullySupportedGroupIDs { get; }
/// <summary>
/// Like <see cref="GetDamagePerType"/>, but indexed by <see cref="DamageTypePrototype.ID"/>
/// </summary>
IReadOnlyDictionary<string, int> GetDamagePerTypeIDs { get; }
/// <summary>
/// Collection of damage types supported by this DamageableComponent.
/// </summary>
/// <remarks>
/// Each of these damage types is fully supported. If any of these damage types is a
/// member of a damage group, these groups are represented in <see cref="ApplicableDamageGroups"></see>
/// </remarks>
HashSet<DamageTypePrototype> SupportedDamageTypes { get; }
/// <summary>
/// Collection of damage groups that are fully supported by DamageableComponent.
/// </summary>
/// <remarks>
/// This describes what damage groups this damage container explicitly supports. It supports every damage type
/// contained in these damage groups. It may also support other damage types not in these groups. To see all
/// damage types <see cref="SupportedDamageTypes"/>, and to see all applicable damage groups <see
/// cref="ApplicableDamageGroups"/>.
/// </remarks>
HashSet<DamageGroupPrototype> FullySupportedDamageGroups { get; }
/// <summary>
/// Collection of damage groups that could apply damage to this DamageableComponent.
/// </summary>
/// <remarks>
/// This describes what damage groups could have an effect on this damage container. However not every damage
/// group has to be fully supported. For example, the container may support ONLY the piercing damage type. It should
/// therefore be affected by instances of brute damage, but does not necessarily support blunt or slash damage.
/// For a list of supported damage types, see <see cref="SupportedDamageTypes"/>.
/// </remarks>
HashSet<DamageGroupPrototype> ApplicableDamageGroups { get; }
/// <summary>
/// The resistances of this component.
/// </summary>
ResistanceSet Resistances { get; }
/// <summary>
/// Tries to get the amount of damage of a type.
/// </summary>
/// <param name="type">The type to get the damage of.</param>
/// <param name="damage">The amount of damage of that type.</param>
/// <returns>
/// True if the given <see cref="type"/> is supported, false otherwise.
/// </returns>
bool TryGetDamage(DamageTypePrototype type, out int damage);
/// <summary>
/// Returns the amount of damage of a given type, or zero if it is not supported.
/// </summary>
int GetDamage(DamageTypePrototype type);
/// <summary>
/// Tries to get the total amount of damage in a damage group.
/// </summary>
/// <param name="group">The group to get the damage of.</param>
/// <param name="damage">The amount of damage in that group.</param>
/// <returns>
/// True if the given group is applicable to this container, false otherwise.
/// </returns>
bool TryGetDamage(DamageGroupPrototype group, out int damage);
/// <summary>
/// Returns the amount of damage present in an applicable group, or zero if no members are supported.
/// </summary>
int GetDamage(DamageGroupPrototype group);
/// <summary>
/// Tries to change the specified <see cref="DamageTypePrototype"/>, applying
/// resistance values only if it is dealing damage.
/// </summary>
/// <param name="type">Type of damage being changed.</param>
/// <param name="amount">
/// Amount of damage being received (positive for damage, negative for heals).
/// </param>
/// <param name="ignoreDamageResistances">
/// Whether or not to ignore resistances when taking damage.
/// Healing always ignores resistances, regardless of this input.
/// </param>
/// <returns>
/// False if the given type is not supported or no damage change occurred; true otherwise.
/// </returns>
bool TryChangeDamage(DamageTypePrototype type, int amount, bool ignoreDamageResistances = false);
/// <summary>
/// Tries to change damage of the specified <see cref="DamageGroupPrototype"/>, applying resistance values
/// only if it is damage.
/// </summary>
/// <remarks>
/// <para>
/// If dealing damage, this spreads the damage change amount evenly between the <see
/// cref="DamageTypePrototype"></see>s in this group (subject to integer rounding). If only a subset of the
/// damage types in the group are actually supported, then the total damage dealt may be less than expected
/// (unsupported damage is ignored).
/// </para>
/// <para>
/// If healing damage, this spreads the damage change proportional to the current damage value of each <see
/// cref="DamageTypePrototype"></see> (subject to integer rounding). If there is less damage than is being
/// healed, some healing is wasted. Unsupported damage types do not waste healing.
/// </para>
/// </remarks>
/// <param name="group">group of damage being changed.</param>
/// <param name="amount">
/// Amount of damage being received (positive for damage, negative for heals).
/// </param>
/// <param name="ignoreDamageResistances">
/// Whether to ignore resistances when taking damage. Healing always ignores resistances, regardless of this
/// input.
/// </param>
/// <returns>
/// Returns false if the given group is not applicable or no damage change occurred; true otherwise.
/// </returns>
bool TryChangeDamage(DamageGroupPrototype group, int amount, bool ignoreDamageResistances = false);
/// <summary>
/// Forcefully sets the specified <see cref="DamageTypePrototype"/> to the given value, ignoring resistance
/// values.
/// </summary>
/// <param name="type">Type of damage being set.</param>
/// <param name="newValue">New damage value to be set.</param>
/// <returns>
/// Returns false if a given type is not supported or a negative value is provided; true otherwise.
/// </returns>
bool TrySetDamage(DamageTypePrototype type, int newValue);
/// <summary>
/// Forcefully sets all damage types in a specified damage group using <see cref="TrySetDamage"></see>.
/// </summary>
/// <remarks>
/// Note that the actual damage of this group will be equal to the given value times the number damage group
/// members that this container supports.
/// </remarks>
/// <param name="group">Group of damage being set.</param>
/// <param name="newValue">New damage value to be set.</param>
/// <returns>
/// Returns false if the given group is not applicable or a negative value is provided; true otherwise.
/// </returns>
bool TrySetDamage(DamageGroupPrototype group, int newValue);
/// <summary>
/// Sets all supported damage types to specified value using <see cref="TrySetDamage"></see>.
/// </summary>
/// <param name="newValue">New damage value to be set.</param>
/// <returns>
/// Returns false if a negative value is provided; true otherwise.
/// </returns>
bool TrySetAllDamage(int newValue);
/// <summary>
/// Returns true if the given damage group is applicable to this damage container.
/// </summary>
public bool IsApplicableDamageGroup(DamageGroupPrototype group);
/// <summary>
/// Returns true if the given damage group is fully supported by this damage container.
/// </summary>
public bool IsFullySupportedDamageGroup(DamageGroupPrototype group);
/// <summary>
/// Returns true if the given damage type is supported by this damage container.
/// </summary>
public bool IsSupportedDamageType(DamageTypePrototype type);
/// <summary>
/// Invokes the HealthChangedEvent with the current values of health.
/// </summary>
void ForceHealthChangedEvent();
}
}

View File

@@ -1,151 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Container
{
/// <summary>
/// A damage container which can be used to specify support for various damage types.
/// </summary>
/// <remarks>
/// This is effectively just a list of damage types that can be specified in YAML files using both damage types
/// and damage groups. Currently this is only used to specify what damage types a <see
/// cref="Components.DamageableComponent"/> should support.
/// </remarks>
[Prototype("damageContainer")]
[Serializable, NetSerializable]
public class DamageContainerPrototype : IPrototype, ISerializationHooks
{
private IPrototypeManager _prototypeManager = default!;
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
/// <summary>
/// Determines whether this DamageContainerPrototype will support ALL damage types and groups. If true,
/// ignore all other options.
/// </summary>
[DataField("supportAll")] private bool _supportAll;
[DataField("supportedGroups")] private HashSet<string> _supportedDamageGroupIDs = new();
[DataField("supportedTypes")] private HashSet<string> _supportedDamageTypeIDs = new();
private HashSet<DamageGroupPrototype> _applicableDamageGroups = new();
private HashSet<DamageGroupPrototype> _fullySupportedDamageGroups = new();
private HashSet<DamageTypePrototype> _supportedDamageTypes = new();
// TODO NET 5 IReadOnlySet
/// <summary>
/// Collection of damage groups that can affect this container.
/// </summary>
/// <remarks>
/// This describes what damage groups can have an effect on this damage container. However not every damage
/// group has to be fully supported. For example, the container may support ONLY the piercing damage type.
/// It should therefore be affected by instances of brute group damage, but does not necessarily support
/// blunt or slash damage. If damage containers are only specified by supported damage groups, and every
/// damage type is in only one damage group, then SupportedDamageTypes should be equal to
/// ApplicableDamageGroups. For a list of supported damage types, see <see cref="SupportedDamageTypes"/>.
/// </remarks>
[ViewVariables] public IReadOnlyCollection<DamageGroupPrototype> ApplicableDamageGroups => _applicableDamageGroups;
/// <summary>
/// Collection of damage groups that are fully supported by this container.
/// </summary>
/// <remarks>
/// This describes what damage groups this damage container explicitly supports. It supports every damage
/// type contained in these damage groups. It may also support other damage types not in these groups. To
/// see all damage types <see cref="SupportedDamageTypes"/>, and to see all applicable damage groups <see
/// cref="ApplicableDamageGroups"/>.
/// </remarks>
[ViewVariables] public IReadOnlyCollection<DamageGroupPrototype> FullySupportedDamageGroups => _fullySupportedDamageGroups;
/// <summary>
/// Collection of damage types supported by this container.
/// </summary>
/// <remarks>
/// Each of these damage types is fully supported by the DamageContainer. If any of these damage types is a
/// member of a damage group, these groups are added to <see cref="ApplicableDamageGroups"></see>
/// </remarks>
[ViewVariables] public IReadOnlyCollection<DamageTypePrototype> SupportedDamageTypes => _supportedDamageTypes;
void ISerializationHooks.AfterDeserialization()
{
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (_supportAll)
{
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
{
_applicableDamageGroups.Add(group);
_fullySupportedDamageGroups.Add(group);
}
foreach (var type in _prototypeManager.EnumeratePrototypes<DamageTypePrototype>())
{
_supportedDamageTypes.Add(type);
}
return;
}
// Add fully supported damage groups
foreach (var groupID in _supportedDamageGroupIDs)
{
var group = _prototypeManager.Index<DamageGroupPrototype>(groupID);
_fullySupportedDamageGroups.Add(group);
foreach (var type in group.DamageTypes)
{
_supportedDamageTypes.Add(type);
}
}
// Add individual damage types, that are either not part of a group, or whose groups are (possibly) not fully supported
foreach (var supportedTypeID in _supportedDamageTypeIDs)
{
var type = _prototypeManager.Index<DamageTypePrototype>(supportedTypeID);
_supportedDamageTypes.Add(type);
}
// For whatever reason, someone may have listed all members of a group as supported instead of just listing
// the group as supported. Check for this.
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
{
if (_fullySupportedDamageGroups.Contains(group))
{
continue;
}
// The group is not in the list of fully supported groups. Should it be?
var allMembersSupported = true;
foreach (var type in group.DamageTypes)
{
if (!_supportedDamageTypes.Contains(type))
{
// not all members are supported
allMembersSupported = false;
break;
}
}
if (allMembersSupported) {
// All members are supported. The silly goose should have just used a damage group.
_fullySupportedDamageGroups.Add(group);
}
}
// For each supported damage type, check whether it is in any existing group, If it is add it to _applicableDamageGroups
foreach (var type in _supportedDamageTypes)
{
foreach (var group in _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>())
{
if (group.DamageTypes.Contains(type))
{
_applicableDamageGroups.Add(group);
}
}
}
}
}
}

View File

@@ -1,31 +0,0 @@
namespace Content.Shared.Damage
{
/// <summary>
/// Data class with information on how the value of a
/// single <see cref="DamageTypePrototype"/> has changed.
/// </summary>
public struct DamageChangeData
{
/// <summary>
/// Type of damage that changed.
/// </summary>
public DamageTypePrototype Type;
/// <summary>
/// The new current value for that damage.
/// </summary>
public int NewValue;
/// <summary>
/// How much the health value changed from its last value (negative is heals, positive is damage).
/// </summary>
public int Delta;
public DamageChangeData(DamageTypePrototype type, int newValue, int delta)
{
Type = type;
NewValue = newValue;
Delta = delta;
}
}
}

View File

@@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Shared.Damage.Components;
namespace Content.Shared.Damage
{
public class DamageChangedEventArgs : EventArgs
{
public DamageChangedEventArgs(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
{
Damageable = damageable;
Data = data;
}
public DamageChangedEventArgs(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta)
{
Damageable = damageable;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
Data = data;
}
/// <summary>
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
/// </summary>
public IDamageableComponent Damageable { get; }
/// <summary>
/// List containing data on each <see cref="DamageTypePrototype"/> that was changed.
/// </summary>
public IReadOnlyList<DamageChangeData> Data { get; }
}
}

View File

@@ -1,51 +0,0 @@
using System.Collections.Generic;
using Content.Shared.Damage.Components;
using Robust.Shared.GameObjects;
namespace Content.Shared.Damage
{
public class DamageChangedMessage : ComponentMessage
{
public DamageChangedMessage(IDamageableComponent damageable, IReadOnlyList<DamageChangeData> data)
{
Damageable = damageable;
Data = data;
}
public DamageChangedMessage(IDamageableComponent damageable, DamageTypePrototype type, int newValue, int delta)
{
Damageable = damageable;
var datum = new DamageChangeData(type, newValue, delta);
var data = new List<DamageChangeData> {datum};
Data = data;
}
/// <summary>
/// Reference to the <see cref="IDamageableComponent"/> that invoked the event.
/// </summary>
public IDamageableComponent Damageable { get; }
/// <summary>
/// List containing data on each <see cref="DamageTypePrototype"/> that was changed.
/// </summary>
public IReadOnlyList<DamageChangeData> Data { get; }
public bool TookDamage
{
get
{
foreach (var datum in Data)
{
if (datum.Delta > 0)
{
return true;
}
}
return false;
}
}
}
}

View File

@@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Damage
{
/// <summary>
/// A Group of <see cref="DamageTypePrototype"/>s.
/// </summary>
/// <remarks>
/// These groups can be used to specify supported damage types of a <see
/// cref="Container.DamageContainerPrototype"/>, or to change/get/set damage in a <see
/// cref="Components.DamageableComponent"/>.
/// </remarks>
[Prototype("damageGroup")]
[Serializable, NetSerializable]
public class DamageGroupPrototype : IPrototype, ISerializationHooks
{
private IPrototypeManager _prototypeManager = default!;
[DataField("id", required: true)] public string ID { get; } = default!;
[DataField("damageTypes", required: true)]
public List<string> TypeIDs { get; } = default!;
public HashSet<DamageTypePrototype> DamageTypes { get; } = new();
// Create set of damage types
void ISerializationHooks.AfterDeserialization()
{
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var typeID in TypeIDs)
{
DamageTypes.Add(_prototypeManager.Index<DamageTypePrototype>(typeID));
}
}
}
}

View File

@@ -0,0 +1,375 @@
using Content.Shared.Damage.Prototypes;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Content.Shared.Damage
{
// TODO DAMAGE UNITS Move this whole class away from, using integers. Also get rid of a lot of the rounding. Just
// use DamageUnit math operators.
/// <summary>
/// This class represents a collection of damage types and damage values.
/// </summary>
/// <remarks>
/// The actual damage information is stored in <see cref="DamageDict"/>. This class provides
/// functions to apply resistance sets and supports basic math operations to modify this dictionary.
/// </remarks>
[DataDefinition]
public class DamageSpecifier
{
[DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, DamageTypePrototype>))]
private readonly Dictionary<string,int>? _damageTypeDictionary;
[DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, DamageGroupPrototype>))]
private readonly Dictionary<string, int>? _damageGroupDictionary;
/// <summary>
/// Main DamageSpecifier dictionary. Most DamageSpecifier functions exist to somehow modifying this.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> DamageDict
{
get
{
if (_damageDict == null)
DeserializeDamage();
return _damageDict!;
}
set => _damageDict = value;
}
private Dictionary<string, int>? _damageDict;
/// <summary>
/// Sum of the damage values.
/// </summary>
/// <remarks>
/// Note that this being zero does not mean this damage has no effect. Healing in one type may cancel damage
/// in another. For this purpose, you should instead use <see cref="TrimZeros()"/> and then check the <see
/// cref="Empty"/> property.
/// </remarks>
public int Total => DamageDict.Values.Sum();
/// <summary>
/// Whether this damage specifier has any entries.
/// </summary>
public bool Empty => DamageDict.Count == 0;
#region constructors
/// <summary>
/// Constructor that just results in an empty dictionary.
/// </summary>
public DamageSpecifier() { }
/// <summary>
/// Constructor that takes another DamageSpecifier instance and copies it.
/// </summary>
public DamageSpecifier(DamageSpecifier damageSpec)
{
DamageDict = new(damageSpec.DamageDict);
}
/// <summary>
/// Constructor that takes a single damage type prototype and a damage value.
/// </summary>
public DamageSpecifier(DamageTypePrototype type, int value)
{
DamageDict = new() { { type.ID, value } };
}
/// <summary>
/// Constructor that takes a single damage group prototype and a damage value. The value is divided between members of the damage group.
/// </summary>
public DamageSpecifier(DamageGroupPrototype group, int value)
{
_damageGroupDictionary = new() { { group.ID, value } };
}
#endregion constructors
/// <summary>
/// Combines the damage group and type datafield dictionaries into a single damage dictionary.
/// </summary>
public void DeserializeDamage()
{
// Add all the damage types by just copying the type dictionary (if it is not null).
if (_damageTypeDictionary != null)
{
_damageDict = new(_damageTypeDictionary);
}
else
{
_damageDict = new();
}
if (_damageGroupDictionary == null)
return;
// Then resolve damage groups and add them
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var entry in _damageGroupDictionary)
{
if (!prototypeManager.TryIndex<DamageGroupPrototype>(entry.Key, out var group))
{
// This can happen if deserialized before prototypes are loaded.
Logger.Error($"Unknown damage group given to DamageSpecifier: {entry.Key}");
continue;
}
// Simply distribute evenly (except for rounding).
// We do this by reducing remaining the # of types and damage every loop.
var remainingTypes = group.DamageTypes.Count;
var remainingDamage = entry.Value;
foreach (var damageType in group.DamageTypes)
{
var damage = remainingDamage / remainingTypes;
if (!_damageDict.TryAdd(damageType, damage))
{
// Key already exists, add values
_damageDict[damageType] += damage;
}
remainingDamage -= damage;
remainingTypes -= 1;
}
}
}
/// <summary>
/// Reduce (or increase) damages by applying a resistance set.
/// </summary>
/// <remarks>
/// Only applies resistance to a damage type if it is dealing damage, not healing.
/// </remarks>
public static DamageSpecifier ApplyResistanceSet(DamageSpecifier damageSpec, ResistanceSetPrototype resistanceSet)
{
// Make a copy of the given data. Don't modify the one passed to this function. I did this before, and weapons became
// duller as you hit walls. Neat, but not intended. And confusing, when you realize your fists don't work no
// more cause they're just bloody stumps.
DamageSpecifier newDamage = new(damageSpec);
foreach (var entry in newDamage.DamageDict)
{
if (entry.Value <= 0) continue;
float newValue = entry.Value;
if (resistanceSet.FlatReduction.TryGetValue(entry.Key, out var reduction))
{
newValue -= reduction;
if (newValue <= 0)
{
// flat reductions cannot heal you
newDamage.DamageDict[entry.Key] = 0;
continue;
}
}
if (resistanceSet.Coefficients.TryGetValue(entry.Key, out var coefficient))
{
// negative coefficients **can** heal you.
newValue = MathF.Round(newValue*coefficient, MidpointRounding.AwayFromZero);
}
newDamage.DamageDict[entry.Key] = (int) newValue;
}
newDamage.TrimZeros();
return newDamage;
}
/// <summary>
/// Remove any damage entries with zero damage.
/// </summary>
public void TrimZeros()
{
foreach (var (key, value) in DamageDict)
{
if (value == 0)
{
DamageDict.Remove(key);
}
}
}
/// <summary>
/// Clamps each damage value to be within the given range.
/// </summary>
public void Clamp(int minValue = 0, int maxValue = 0)
{
DebugTools.Assert(minValue < maxValue);
ClampMax(maxValue);
ClampMin(minValue);
}
/// <summary>
/// Sets all damage values to be at least as large as the given number.
/// </summary>
/// <remarks>
/// Note that this only acts on damage types present in the dictionary. It will not add new damage types.
/// </remarks>
public void ClampMin(int minValue = 0)
{
foreach (var (key, value) in DamageDict)
{
if (value < minValue)
{
DamageDict[key] = minValue;
}
}
}
/// <summary>
/// Sets all damage values to be at most some number. Note that if a damage type is not present in the
/// dictionary, these will not be added.
/// </summary>
public void ClampMax(int maxValue = 0)
{
foreach (var (key, value) in DamageDict)
{
if (value > maxValue)
{
DamageDict[key] = maxValue;
}
}
}
/// <summary>
/// This adds the damage values of some other <see cref="DamageSpecifier"/> to the current one without
/// adding any new damage types.
/// </summary>
/// <remarks>
/// This is used for <see cref="DamageableComponent"/>s, such that only "supported" damage types are
/// actually added to the component. In most other instances, you can just use the addition operator.
/// </remarks>
public void ExclusiveAdd(DamageSpecifier other)
{
foreach (var (type, value) in other.DamageDict)
{
if (DamageDict.ContainsKey(type))
{
DamageDict[type] += value;
}
}
}
/// <summary>
/// Add up all the damage values for damage types that are members of a given group.
/// </summary>
/// <remarks>
/// If no members of the group are included in this specifier, returns false.
/// </remarks>
public bool TryGetDamageInGroup(DamageGroupPrototype group, out int total)
{
bool containsMemeber = false;
total = 0;
foreach (var type in group.DamageTypes)
{
if (DamageDict.TryGetValue(type, out var value))
{
total += value;
containsMemeber = true;
}
}
return containsMemeber;
}
/// <summary>
/// Returns a dictionary using <see cref="DamageGroupPrototype.ID"/> keys, with values calculated by adding
/// up the values for each damage type in that group
/// </summary>
/// <remarks>
/// If a damage type is associated with more than one supported damage group, it will contribute to the
/// total of each group. If no members of a group are present in this <see cref="DamageSpecifier"/>, the
/// group is not included in the resulting dictionary.
/// </remarks>
public Dictionary<string, int> GetDamagePerGroup()
{
var damageGroupDict = new Dictionary<string, int>();
foreach (var group in IoCManager.Resolve<IPrototypeManager>().EnumeratePrototypes<DamageGroupPrototype>())
{
if (TryGetDamageInGroup(group, out var value))
{
damageGroupDict.Add(group.ID, value);
}
}
return damageGroupDict;
}
#region Operators
public static DamageSpecifier operator *(DamageSpecifier damageSpec, int factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, entry.Value * factor);
}
return newDamage;
}
public static DamageSpecifier operator *(DamageSpecifier damageSpec, float factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value * factor, MidpointRounding.AwayFromZero));
}
return newDamage;
}
public static DamageSpecifier operator /(DamageSpecifier damageSpec, int factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value / (float) factor, MidpointRounding.AwayFromZero));
}
return newDamage;
}
public static DamageSpecifier operator /(DamageSpecifier damageSpec, float factor)
{
DamageSpecifier newDamage = new();
foreach (var entry in damageSpec.DamageDict)
{
newDamage.DamageDict.Add(entry.Key, (int) MathF.Round(entry.Value / factor, MidpointRounding.AwayFromZero));
}
return newDamage;
}
public static DamageSpecifier operator +(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB)
{
// Copy existing dictionary from dataA
DamageSpecifier newDamage = new(damageSpecA);
// Then just add types in B
foreach (var entry in damageSpecB.DamageDict)
{
if (!newDamage.DamageDict.TryAdd(entry.Key, entry.Value))
{
// Key already exists, add values
newDamage.DamageDict[entry.Key] += entry.Value;
}
}
return newDamage;
}
public static DamageSpecifier operator -(DamageSpecifier damageSpecA, DamageSpecifier damageSpecB) => damageSpecA + -damageSpecB;
public static DamageSpecifier operator +(DamageSpecifier damageSpec) => damageSpec;
public static DamageSpecifier operator -(DamageSpecifier damageSpec) => damageSpec * -1;
public static DamageSpecifier operator *(float factor, DamageSpecifier damageSpec) => damageSpec * factor;
public static DamageSpecifier operator *(int factor, DamageSpecifier damageSpec) => damageSpec * factor;
}
#endregion
}

View File

@@ -1,13 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Shared.Damage
{
[UsedImplicitly]
public class DamageSystem : EntitySystem
{
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using Content.Shared.Acts;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Radiation;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage
{
/// <summary>
/// Component that allows entities to take damage.
/// </summary>
/// <remarks>
/// The supported damage types are specified using a <see cref="DamageContainerPrototype"/>s. DamageContainers
/// may also have resistances to certain damage types, defined via a <see cref="ResistanceSetPrototype"/>.
/// </remarks>
[RegisterComponent]
[NetworkedComponent()]
[Friend(typeof(DamageableSystem))]
public class DamageableComponent : Component, IRadiationAct, IExAct
{
public override string Name => "Damageable";
/// <summary>
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
/// If null, all damage types will be supported.
/// </summary>
[DataField("damageContainer", customTypeSerializer: typeof(PrototypeIdSerializer<DamageContainerPrototype>))]
public string? DamageContainerID;
/// <summary>
/// This <see cref="ResistanceSetPrototype"/> will be applied to any damage that is dealt to this container,
/// unless the damage explicitly ignores resistances.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("resistanceSet", customTypeSerializer: typeof(PrototypeIdSerializer<ResistanceSetPrototype>))]
public string? ResistanceSetID;
/// <summary>
/// All the damage information is stored in this <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// If this data-field is specified, this allows damageable components to be initialized with non-zero damage.
/// </remarks>
[DataField("damage")]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = new();
/// <summary>
/// Damage, indexed by <see cref="DamageGroupPrototype"/> ID keys.
/// </summary>
/// <remarks>
/// Groups which have no members that are supported by this component will not be present in this
/// dictionary.
/// </remarks>
[ViewVariables] public Dictionary<string, int> DamagePerGroup = new();
/// <summary>
/// The sum of all damages in the DamageableComponent.
/// </summary>
[ViewVariables] public int TotalDamage;
// Really these shouldn't be here. OnExplosion() and RadiationAct() should be handled elsewhere.
[ViewVariables]
[DataField("radiationDamageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> RadiationDamageTypeIDs = new() {"Radiation"};
[ViewVariables]
[DataField("explosionDamageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> ExplosionDamageTypeIDs = new() { "Piercing", "Heat" };
// TODO RADIATION Remove this.
void IRadiationAct.RadiationAct(float frameTime, SharedRadiationPulseComponent radiation)
{
var damageValue = Math.Max((int) (frameTime * radiation.RadsPerSecond), 1);
// Radiation should really just be a damage group instead of a list of types.
DamageSpecifier damage = new();
foreach (var typeID in ExplosionDamageTypeIDs)
{
damage.DamageDict.Add(typeID, damageValue);
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, damage);
}
// TODO EXPLOSION Remove this.
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
{
var damageValue = eventArgs.Severity switch
{
ExplosionSeverity.Light => 20,
ExplosionSeverity.Heavy => 60,
ExplosionSeverity.Destruction => 250,
_ => throw new ArgumentOutOfRangeException()
};
// Explosion should really just be a damage group instead of a list of types.
DamageSpecifier damage = new();
foreach (var typeID in ExplosionDamageTypeIDs)
{
damage.DamageDict.Add(typeID, damageValue);
}
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, damage);
}
}
[Serializable, NetSerializable]
public class DamageableComponentState : ComponentState
{
public readonly Dictionary<string, int> DamageDict;
public readonly string? ResistanceSetID;
public DamageableComponentState(
Dictionary<string, int> damageDict,
string? resistanceSetID)
{
DamageDict = damageDict;
ResistanceSetID = resistanceSetID;
}
}
}

View File

@@ -0,0 +1,243 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
namespace Content.Shared.Damage
{
public class DamageableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
SubscribeLocalEvent<DamageableComponent, ComponentHandleState>(DamageableHandleState);
SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
}
/// <summary>
/// Initialize a damageable component
/// </summary>
private void DamageableInit(EntityUid uid, DamageableComponent component, ComponentInit _)
{
if (component.DamageContainerID != null &&
_prototypeManager.TryIndex<DamageContainerPrototype>(component.DamageContainerID,
out var damageContainerPrototype))
{
// Initialize damage dictionary, using the types and groups from the damage
// container prototype
foreach (var type in damageContainerPrototype.SupportedTypes)
{
component.Damage.DamageDict.TryAdd(type, 0);
}
foreach (var groupID in damageContainerPrototype.SupportedGroups)
{
var group = _prototypeManager.Index<DamageGroupPrototype>(groupID);
foreach (var type in group.DamageTypes)
{
component.Damage.DamageDict.TryAdd(type, 0);
}
}
}
else
{
// No DamageContainerPrototype was given. So we will allow the container to support all damage types
foreach (var type in _prototypeManager.EnumeratePrototypes<DamageTypePrototype>())
{
component.Damage.DamageDict.TryAdd(type.ID, 0);
}
}
component.DamagePerGroup = component.Damage.GetDamagePerGroup();
component.TotalDamage = component.Damage.Total;
}
/// <summary>
/// Directly sets the damage specifier of a damageable component.
/// </summary>
/// <remarks>
/// Useful for some unfriendly folk. Also ensures that cached values are updated and that a damage changed
/// event is raised.
/// </remarks>
public void SetDamage(DamageableComponent damageable, DamageSpecifier damage)
{
damageable.Damage = damage;
DamageChanged(damageable);
}
/// <summary>
/// If the damage in a DamageableComponent was changed, this function should be called.
/// </summary>
/// <remarks>
/// This updates cached damage information, flags the component as dirty, and raises a damage changed event.
/// The damage changed event is used by other systems, such as damage thresholds.
/// </remarks>
public void DamageChanged(DamageableComponent component, DamageSpecifier? damageDelta = null)
{
component.DamagePerGroup = component.Damage.GetDamagePerGroup();
component.TotalDamage = component.Damage.Total;
component.Dirty();
RaiseLocalEvent(component.Owner.Uid, new DamageChangedEvent(component, damageDelta), false);
}
/// <summary>
/// Applies damage specified via a <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// <see cref="DamageSpecifier"/> is effectively just a dictionary of damage types and damage values. This
/// function just applies the container's resistances (unless otherwise specified) and then changes the
/// stored damage data. Division of group damage into types is managed by <see cref="DamageSpecifier"/>.
/// </remarks>
/// <returns>
/// Returns a <see cref="DamageSpecifier"/> with information about the actual damage changes. This will be
/// null if the user had no applicable components that can take damage.
/// </returns>
public DamageSpecifier? TryChangeDamage(EntityUid uid, DamageSpecifier damage, bool ignoreResistances = false)
{
if (!ComponentManager.TryGetComponent<DamageableComponent>(uid, out var damageable))
{
// TODO BODY SYSTEM pass damage onto body system
return null;
}
if (damage == null)
{
Logger.Error("Null DamageSpecifier. Probably because a required yaml field was not given.");
return null;
}
if (damage.Empty)
{
return damage;
}
// Apply resistances
if (!ignoreResistances && damageable.ResistanceSetID != null)
{
if (_prototypeManager.TryIndex<ResistanceSetPrototype>(damageable.ResistanceSetID, out var resistanceSet))
{
damage = DamageSpecifier.ApplyResistanceSet(damage, resistanceSet);
}
if (damage.Empty)
{
return damage;
}
}
// Copy the current damage, for calculating the difference
DamageSpecifier oldDamage = new(damageable.Damage);
damageable.Damage.ExclusiveAdd(damage);
damageable.Damage.ClampMin(0);
var delta = damageable.Damage - oldDamage;
delta.TrimZeros();
if (!delta.Empty)
{
DamageChanged(damageable, delta);
}
return delta;
}
/// <summary>
/// Sets all damage types supported by a <see cref="DamageableComponent"/> to the specified value.
/// </summary>
/// <remakrs>
/// Does nothing If the given damage value is negative.
/// </remakrs>
public void SetAllDamage(DamageableComponent component, int newValue)
{
if (newValue < 0)
{
// invalid value
return;
}
foreach (var type in component.Damage.DamageDict.Keys)
{
component.Damage.DamageDict[type] = newValue;
}
// Setting damage does not count as 'dealing' damage, even if it is set to a larger value, so we pass an
// empty damage delta.
DamageChanged(component, new DamageSpecifier());
}
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, component.ResistanceSetID);
}
private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args)
{
if (args.Current is not DamageableComponentState state)
{
return;
}
component.ResistanceSetID = state.ResistanceSetID;
// Has the damage actually changed?
DamageSpecifier newDamage = new() { DamageDict = state.DamageDict };
var delta = component.Damage - newDamage;
delta.TrimZeros();
if (!delta.Empty)
{
component.Damage = newDamage;
DamageChanged(component, delta);
}
}
}
public class DamageChangedEvent : EntityEventArgs
{
/// <summary>
/// This is the component whose damage was changed.
/// </summary>
/// <remarks>
/// Given that nearly every component that cares about a change in the damage, needs to know the
/// current damage values, directly passing this information prevents a lot of duplicate
/// Owner.TryGetComponent() calls.
/// </remarks>
public readonly DamageableComponent Damageable;
/// <summary>
/// The amount by which the damage has changed. If the damage was set directly to some number, this will be
/// null.
/// </summary>
public readonly DamageSpecifier? DamageDelta;
/// <summary>
/// Was any of the damage change dealing damage, or was it all healing?
/// </summary>
public readonly bool DamageIncreased = false;
public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta)
{
Damageable = damageable;
DamageDelta = damageDelta;
if (DamageDelta == null)
return;
foreach (var damageChange in DamageDelta.DamageDict.Values)
{
if (damageChange > 0)
{
DamageIncreased = true;
break;
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// A damage container which can be used to specify support for various damage types.
/// </summary>
/// <remarks>
/// This is effectively just a list of damage types that can be specified in YAML files using both damage types
/// and damage groups. Currently this is only used to specify what damage types a <see
/// cref="DamageableComponent"/> should support.
/// </remarks>
[Prototype("damageContainer")]
[Serializable, NetSerializable]
public class DamageContainerPrototype : IPrototype
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
/// <summary>
/// List of damage groups that are supported by this container.
/// </summary>
[DataField("supportedGroups", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageGroupPrototype>))]
public List<string> SupportedGroups = new();
/// <summary>
/// Partial List of damage types supported by this container. Note that members of the damage groups listed
/// in <see cref="SupportedGroups"/> are also supported, but they are not included in this list.
/// </summary>
[DataField("supportedTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> SupportedTypes = new();
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// A Group of <see cref="DamageTypePrototype"/>s.
/// </summary>
/// <remarks>
/// These groups can be used to specify supported damage types of a <see cref="DamageContainerPrototype"/>, or
/// to change/get/set damage in a <see cref="DamageableComponent"/>.
/// </remarks>
[Prototype("damageGroup")]
[Serializable, NetSerializable]
public class DamageGroupPrototype : IPrototype
{
[DataField("id", required: true)] public string ID { get; } = default!;
[DataField("damageTypes", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string> DamageTypes { get; } = default!;
}
}

View File

@@ -3,7 +3,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Damage
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// A single damage type. These types are grouped together in <see cref="DamageGroupPrototype"/>s.

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Prototypes
{
/// <summary>
/// Prototype of damage resistance sets. Can be applied to <see cref="DamageSpecifier"/> using <see
/// cref="DamageSpecifier.ApplyResistanceSet(ResistanceSetPrototype)"/>. This can be done several times as the
/// <see cref="DamageSpecifier"/> is passed to it's final target. By default the receiving <see cref="DamageableComponent"/>, will
/// also apply it's own <see cref="ResistanceSetPrototype"/>.
/// </summary>
[Prototype("resistanceSet")]
[Serializable, NetSerializable]
public class ResistanceSetPrototype : IPrototype
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[DataField("coefficients", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<float, DamageTypePrototype>))]
public Dictionary<string, float> Coefficients = new();
[DataField("flatReductions", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<float, DamageTypePrototype>))]
public Dictionary<string, float> FlatReduction = new();
}
}

View File

@@ -1,66 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Resistances
{
/// <summary>
/// Set of resistances used by damageable objects.
/// Each <see cref="DamageTypePrototype"/> has a multiplier and flat damage
/// reduction value.
/// </summary>
[Serializable, NetSerializable]
public class ResistanceSet
{
[ViewVariables]
public string? ID { get; } = string.Empty;
[ViewVariables]
public Dictionary<DamageTypePrototype, ResistanceSetSettings> Resistances { get; } = new();
public ResistanceSet()
{
}
public ResistanceSet(ResistanceSetPrototype data)
{
ID = data.ID;
Resistances = data.Resistances;
}
/// <summary>
/// Adjusts input damage with the resistance set values.
/// Only applies reduction if the amount is damage (positive), not
/// healing (negative).
/// </summary>
/// <param name="damageType">Type of damage.</param>
/// <param name="amount">Incoming amount of damage.</param>
public int CalculateDamage(DamageTypePrototype damageType, int amount)
{
// Do nothing if the damage type is not specified in resistance set.
if (!Resistances.TryGetValue(damageType, out var resistance))
{
return amount;
}
if (amount > 0) // Only apply reduction if it's healing, not damage.
{
amount -= resistance.FlatReduction;
if (amount <= 0)
{
return 0;
}
}
amount = (int) Math.Ceiling(amount * resistance.Coefficient);
return amount;
}
}
}

View File

@@ -1,61 +0,0 @@
using System;
using System.CodeDom;
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Damage.Resistances
{
/// <summary>
/// Prototype for the BodyPart class.
/// </summary>
[Prototype("resistanceSet")]
[Serializable, NetSerializable]
public class ResistanceSetPrototype : IPrototype, ISerializationHooks
{
[ViewVariables]
[DataField("id", required: true)]
public string ID { get; } = default!;
[ViewVariables]
[DataField("coefficients", required: true)]
private Dictionary<string, float> coefficients { get; } = new();
[ViewVariables]
[DataField("flatReductions", required: true)]
private Dictionary<string, int> flatReductions { get; } = new();
[ViewVariables]
public Dictionary<DamageTypePrototype, ResistanceSetSettings> Resistances { get; private set; } = new();
void ISerializationHooks.AfterDeserialization()
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var damageTypeID in coefficients.Keys)
{
var resolvedDamageType = prototypeManager.Index<DamageTypePrototype>(damageTypeID);
Resistances.Add(resolvedDamageType, new ResistanceSetSettings(coefficients[damageTypeID], flatReductions[damageTypeID]));
}
}
}
/// <summary>
/// Resistance Settings for a specific DamageType. Flat reduction should always be applied before the coefficient.
/// </summary>
[Serializable, NetSerializable]
public readonly struct ResistanceSetSettings
{
[ViewVariables] public readonly float Coefficient;
[ViewVariables] public readonly int FlatReduction;
public ResistanceSetSettings(float coefficient, int flatReduction)
{
Coefficient = coefficient;
FlatReduction = flatReduction;
}
}
}

View File

@@ -16,25 +16,24 @@ namespace Content.Shared.MedicalScanner
public class MedicalScannerBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly EntityUid? Entity;
public readonly Dictionary<string, int> DamagePerSupportedGroupID;
public readonly Dictionary<string, int> DamagePerTypeID;
public readonly IReadOnlyDictionary<string, int> DamagePerGroup;
public readonly IReadOnlyDictionary<string, int> DamagePerType;
public readonly bool IsScanned;
public MedicalScannerBoundUserInterfaceState(
EntityUid? entity,
Dictionary<string, int> damagePerSupportedGroupID,
Dictionary<string, int> damagePerTypeID,
DamageableComponent? damageable,
bool isScanned)
{
Entity = entity;
DamagePerSupportedGroupID = damagePerSupportedGroupID;
DamagePerTypeID = damagePerTypeID;
DamagePerGroup = damageable?.DamagePerGroup ?? new();
DamagePerType = damageable?.Damage?.DamageDict ?? new();
IsScanned = isScanned;
}
public bool HasDamage()
{
return DamagePerSupportedGroupID.Count > 0 || DamagePerTypeID.Count > 0;
return DamagePerType.Count > 0;
}
}

View File

@@ -5,7 +5,6 @@ using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
@@ -17,18 +16,20 @@ using Robust.Shared.ViewVariables;
namespace Content.Shared.MobState.Components
{
/// <summary>
/// When attached to an <see cref="IDamageableComponent"/>,
/// When attached to an <see cref="DamageableComponent"/>,
/// this component will handle critical and death behaviors for mobs.
/// Additionally, it handles sending effects to clients
/// (such as blur effect for unconsciousness) and managing the health HUD.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IMobStateComponent))]
[NetworkedComponent()]
public abstract class SharedMobStateComponent : Component, IMobStateComponent, IActionBlocker
public class MobStateComponent : Component, IMobStateComponent, IActionBlocker
{
public override string Name => "MobState";
/// <summary>
/// States that this <see cref="SharedMobStateComponent"/> mapped to
/// States that this <see cref="MobStateComponent"/> mapped to
/// the amount of damage at which they are triggered.
/// A threshold is reached when the total damage of an entity is equal
/// to or higher than the int key, but lower than the next threshold.
@@ -36,7 +37,7 @@ namespace Content.Shared.MobState.Components
/// </summary>
[ViewVariables]
[DataField("thresholds")]
private readonly SortedDictionary<int, IMobState> _lowestToHighestStates = default!;
private readonly SortedDictionary<int, IMobState> _lowestToHighestStates = new();
// TODO Remove Nullability?
[ViewVariables]
@@ -53,7 +54,13 @@ namespace Content.Shared.MobState.Components
if (CurrentState != null && CurrentThreshold != null)
{
UpdateState(null, (CurrentState, CurrentThreshold.Value));
// Initialize with given states
SetMobState(null, (CurrentState, CurrentThreshold.Value));
}
else
{
// Initialize with some amount of damage, defaulting to 0.
UpdateState(Owner.GetComponentOrNull<DamageableComponent>()?.TotalDamage ?? 0);
}
}
@@ -88,29 +95,11 @@ namespace Content.Shared.MobState.Components
if (state.CurrentThreshold == null)
{
RemoveState(true);
RemoveState();
}
else
{
UpdateState(state.CurrentThreshold.Value, true);
}
}
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case DamageChangedMessage msg:
if (msg.Damageable.Owner != Owner)
{
break;
}
UpdateState(msg.Damageable.TotalDamage);
break;
UpdateState(state.CurrentThreshold.Value);
}
}
@@ -136,7 +125,7 @@ namespace Content.Shared.MobState.Components
public (IMobState state, int threshold)? GetState(int damage)
{
foreach (var (threshold, state) in _lowestToHighestStates.Reverse())
foreach (var (threshold, state) in _highestToLowestStates)
{
if (damage >= threshold)
{
@@ -273,36 +262,32 @@ namespace Content.Shared.MobState.Components
return TryGetState(earliestState, out state, out threshold);
}
private void RemoveState(bool syncing = false)
private void RemoveState()
{
var old = CurrentState;
CurrentState = null;
CurrentThreshold = null;
UpdateState(old, null);
if (!syncing)
{
Dirty();
}
SetMobState(old, null);
}
public void UpdateState(int damage, bool syncing = false)
/// <summary>
/// Updates the mob state..
/// </summary>
public void UpdateState(int damage)
{
if (!TryGetState(damage, out var newState, out var threshold))
{
return;
}
UpdateState(CurrentState, (newState, threshold));
if (!syncing)
{
Dirty();
}
SetMobState(CurrentState, (newState, threshold));
}
private void UpdateState(IMobState? old, (IMobState state, int threshold)? current)
/// <summary>
/// Sets the mob state and marks the component as dirty.
/// </summary>
private void SetMobState(IMobState? old, (IMobState state, int threshold)? current)
{
if (!current.HasValue)
{
@@ -328,9 +313,10 @@ namespace Content.Shared.MobState.Components
state.UpdateState(Owner, threshold);
var message = new MobStateChangedMessage(this, old, state);
SendMessage(message);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
Dirty();
}
bool IActionBlocker.CanInteract()

View File

@@ -0,0 +1,45 @@
using Content.Shared.Damage;
using Content.Shared.MobState.Components;
using Content.Shared.MobState.State;
using Content.Shared.Movement;
using Content.Shared.Pulling.Events;
using Robust.Shared.GameObjects;
namespace Content.Shared.MobState.EntitySystems
{
public class MobStateSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MobStateComponent, StartPullAttemptEvent>(OnStartPullAttempt);
SubscribeLocalEvent<MobStateComponent, DamageChangedEvent>(UpdateState);
SubscribeLocalEvent<MobStateComponent, MovementAttemptEvent>(OnMoveAttempt);
}
private void OnStartPullAttempt(EntityUid uid, MobStateComponent component, StartPullAttemptEvent args)
{
if(component.IsIncapacitated())
args.Cancel();
}
public void UpdateState(EntityUid _, MobStateComponent component, DamageChangedEvent args)
{
component.UpdateState(args.Damageable.TotalDamage);
}
private void OnMoveAttempt(EntityUid uid, MobStateComponent component, MovementAttemptEvent args)
{
switch (component.CurrentState)
{
case SharedCriticalMobState:
case SharedDeadMobState:
args.Cancel();
return;
default:
return;
}
}
}
}

View File

@@ -1,38 +0,0 @@
using Content.Shared.MobState.Components;
using Content.Shared.MobState.State;
using Content.Shared.Movement;
using Content.Shared.Pulling.Events;
using Robust.Shared.GameObjects;
namespace Content.Shared.MobState.EntitySystems
{
public class SharedMobStateSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedMobStateComponent, StartPullAttemptEvent>(OnStartPullAttempt);
SubscribeLocalEvent<SharedMobStateComponent, MovementAttemptEvent>(OnMoveAttempt);
}
private void OnStartPullAttempt(EntityUid uid, SharedMobStateComponent component, StartPullAttemptEvent args)
{
if(component.IsIncapacitated())
args.Cancel();
}
private void OnMoveAttempt(EntityUid uid, SharedMobStateComponent component, MovementAttemptEvent args)
{
switch (component.CurrentState)
{
case SharedCriticalMobState:
case SharedDeadMobState:
args.Cancel();
return;
default:
return;
}
}
}
}

View File

@@ -44,6 +44,6 @@ namespace Content.Shared.MobState
[NotNullWhen(true)] out IMobState? state,
out int threshold);
void UpdateState(int damage, bool syncing = false);
void UpdateState(int damage);
}
}

View File

@@ -0,0 +1,293 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using System.Collections.Generic;
namespace Content.Tests.Shared
{
// Basic tests of various damage prototypes and classes.
[TestFixture]
[TestOf(typeof(DamageSpecifier))]
[TestOf(typeof(ResistanceSetPrototype))]
[TestOf(typeof(DamageGroupPrototype))]
public class DamageTest : ContentUnitTest
{
static private Dictionary<string, float> _resistanceCoefficientDict = new()
{
// "missing" blunt entry
{ "Piercing", -2 },// Turn Piercing into Healing
{ "Slash", 3 },
{ "Radiation", 1.06f }, // Small change, paired with fractional reduction
};
static private Dictionary<string, float> _resistanceReductionDict = new()
{
{ "Blunt", - 5 },
// "missing" piercing entry
{ "Slash", 8 },
{ "Radiation", 0.5f }, // Fractional adjustment
};
private IPrototypeManager _prototypeManager;
private DamageSpecifier _damageSpec;
[OneTimeSetUp]
public void OneTimeSetup()
{
IoCManager.Resolve<ISerializationManager>().Initialize();
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
_prototypeManager.Initialize();
_prototypeManager.LoadString(_damagePrototypes);
_prototypeManager.Resync();
// Create a damage data set
_damageSpec = new(_prototypeManager.Index<DamageGroupPrototype>("Brute"), 6);
_damageSpec += new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Radiation"), 3);
_damageSpec += new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Slash"), -1); // already exists in brute
}
//Check that DamageSpecifier will split groups and can do arithmetic operations
[Test]
public void DamageSpecifierTest()
{
// Create a copy of the damage data
DamageSpecifier damageSpec = new(_damageSpec);
// Check that it properly split up the groups into types
int damage;
Assert.That(damageSpec.Total, Is.EqualTo(8));
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(2));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(2));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(1));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(3));
// check that integer multiplication works
damageSpec = damageSpec * 2;
Assert.That(damageSpec.Total, Is.EqualTo(16));
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(4));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(4));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(2));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(6));
// check that float multiplication works
damageSpec = damageSpec * 2.2f;
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(9));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(9));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(4));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(13));
Assert.That(damageSpec.Total, Is.EqualTo(9 + 9 + 4 + 13));
// check that integer division works
damageSpec = damageSpec / 2;
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(5));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(5));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(2));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(7));
// check that float division works
damageSpec = damageSpec / 2.4f;
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(2));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(2));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(1));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(3));
// Lets also test the constructor with damage types and damage groups works properly.
damageSpec = new(_prototypeManager.Index<DamageGroupPrototype>("Brute"), 4);
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(1));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(2)); // integer rounding. Piercing is defined as last group member in yaml.
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(1));
damageSpec = new(_prototypeManager.Index<DamageTypePrototype>("Piercing"), 4);
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(4));
}
//Check that DamageSpecifier will be properly adjusted by a resistance set
[Test]
public void ResistanceSetTest()
{
// Create a copy of the damage data
DamageSpecifier damageSpec = 10 * new DamageSpecifier(_damageSpec);
// Create a resistance set
ResistanceSetPrototype resistanceSet = new()
{
Coefficients = _resistanceCoefficientDict,
FlatReduction = _resistanceReductionDict
};
//damage is initially 20 / 20 / 10 / 30
//Each time we subtract -5 / 0 / 8 / 0.5
//then multiply by 1 / -2 / 3 / 1.06
// Apply once
damageSpec = DamageSpecifier.ApplyResistanceSet(damageSpec, resistanceSet);
Assert.That(damageSpec.DamageDict["Blunt"], Is.EqualTo(25));
Assert.That(damageSpec.DamageDict["Piercing"], Is.EqualTo(-40)); // became healing
Assert.That(damageSpec.DamageDict["Slash"], Is.EqualTo(6));
Assert.That(damageSpec.DamageDict["Radiation"], Is.EqualTo(31)); // would be 32 w/o fraction adjustment
// And again, checking for some other behavior
damageSpec = DamageSpecifier.ApplyResistanceSet(damageSpec, resistanceSet);
Assert.That(damageSpec.DamageDict["Blunt"], Is.EqualTo(30));
Assert.That(damageSpec.DamageDict["Piercing"], Is.EqualTo(-40)); // resistances don't apply to healing
Assert.That(!damageSpec.DamageDict.ContainsKey("Slash")); // Reduction reduced to 0, and removed from specifier
Assert.That(damageSpec.DamageDict["Radiation"], Is.EqualTo(32));
}
// Default damage Yaml
private string _damagePrototypes = @"
- 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: resistanceSet
id: Metallic
coefficients:
Blunt: 0.7
Slash: 0.5
Piercing: 0.7
Shock: 1.2
flatReductions:
Blunt: 5
- type: resistanceSet
id: Inflatable
coefficients:
Blunt: 0.5
Piercing: 2.0
Heat: 0.5
Shock: 0
flatReductions:
Blunt: 5
- type: resistanceSet
id: Glass
coefficients:
Blunt: 0.5
Slash: 0.5
Piercing: 0.5
Heat: 0
Shock: 0
flatReductions:
Blunt: 5
- type: damageContainer
id: Biological
supportedGroups:
- Brute
- Burn
- Toxin
- Airloss
- Genetic
- type: damageContainer
id: Inorganic
supportedGroups:
- Brute
supportedTypes:
- Heat
- Shock
";
}
}

View File

@@ -103,6 +103,8 @@
behaviors:
- !type:LungBehavior {}
# TODO DAMAGE UNITS. Some of these damage effects were scaled up to integers.
# Scale back down when damage units are in.
- type: entity
id: OrganHumanHeart
parent: BaseHumanOrgan
@@ -124,36 +126,40 @@
Arithrazine:
effects:
- !type:HealthChange
damageGroup: Toxin
healthChange: -1
- !type:HealthChange
damageGroup: Brute
healthChange: 0.5
damage:
groups:
Toxin: -2 # -1 Multiplying by 2. pls give damage units
Brute: 1 # 0.5
Bicaridine:
effects:
- !type:HealthChange
damageGroup: Brute
healthChange: -2
damage:
groups:
Brute: -2
Dermaline:
effects:
- !type:HealthChange
damageGroup: Burn
healthChange: -3
damage:
groups:
Burn: -3
Dexalin:
effects:
- !type:HealthChange
damageGroup: Airloss
healthChange: -1
damage:
types:
Asphyxiation: -1
DexalinPlus:
effects:
- !type:HealthChange
damageGroup: Airloss
healthChange: -3
damage:
types:
Asphyxiation: -3
Dylovene:
effects:
- !type:HealthChange
damageGroup: Toxin
healthChange: -1
damage:
types:
Poison: -1
Ephedrine:
effects:
- !type:MovespeedModifier
@@ -162,45 +168,45 @@
HeartbreakerToxin:
effects:
- !type:HealthChange
damageGroup: Airloss
healthChange: 4
damage:
types:
Asphyxiation: 4
Kelotane:
effects:
- !type:HealthChange
damageGroup: Burn
healthChange: -1
damage:
groups:
Burn: -1
Lexorin:
effects:
- !type:HealthChange
damageGroup: Airloss
healthChange: 7
damage:
groups:
Airloss: 7
Meth:
effects:
- !type:HealthChange
healthChange: 2.5
damageGroup: Toxin
damage:
types:
Poison: 3 # 2.5
- !type:MovespeedModifier
walkSpeedModifier: 1.3
sprintSpeedModifier: 1.3
Omnizine:
effects:
- !type:HealthChange
healthChange: -2
damageGroup: Burn
- !type:HealthChange
healthChange: -2
damageGroup: Toxin
- !type:HealthChange
healthChange: -2
damageGroup: Airloss
- !type:HealthChange
healthChange: -2
damageGroup: Brute
damage:
groups:
Burn: -3 # -2. w/o damage units did not divide into 3 types
Toxin: -2
Airloss: -2
Brute: -3 # -2. w/o damage units did not divide into 3 types
Synaptizine:
effects:
- !type:HealthChange
damageGroup: Toxin
healthChange: 0.5
damage:
types:
Poison: 1 # 0.5 pls damage units
- type: entity
id: OrganHumanStomach
@@ -261,8 +267,9 @@
effects:
- !type:SatiateThirst
- !type:HealthChange
damageGroup: Toxin
healthChange: 1
damage:
types:
Poison: 1
JuiceWatermelon:
effects:
- !type:SatiateThirst
@@ -306,8 +313,9 @@
- !type:SatiateThirst
hydrationFactor: 2
- !type:HealthChange
damageGroup: Toxin
healthChange: 1
damage:
types:
Poison: 1
- type: entity
id: OrganHumanLiver

Some files were not shown because too many files have changed in this diff Show More