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.Components;
using Content.Shared.Body.Mechanism; using Content.Shared.Body.Mechanism;
using Content.Shared.Body.Part; using Content.Shared.Body.Part;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -147,7 +147,7 @@ namespace Content.Client.Body.UI
BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Owner.Name)}"; BodyPartLabel.Text = $"{Loc.GetString(slotName)}: {Loc.GetString(part.Owner.Name)}";
// TODO BODY Part damage // 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)); 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.Client.HealthOverlay.UI;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.MobState; using Content.Shared.MobState;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -79,7 +79,7 @@ namespace Content.Client.HealthOverlay
var viewBox = _eyeManager.GetWorldViewport().Enlarged(2.0f); 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; var entity = mobState.Owner;

View File

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

View File

@@ -1,6 +1,5 @@
using System.Text; using System.Text;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Shared.Damage;
using System.Linq; using System.Linq;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
@@ -10,6 +9,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using static Content.Shared.MedicalScanner.SharedMedicalScannerComponent; using static Content.Shared.MedicalScanner.SharedMedicalScannerComponent;
using static Robust.Client.UserInterface.Controls.BoxContainer; using static Robust.Client.UserInterface.Controls.BoxContainer;
using Content.Shared.Damage.Prototypes;
namespace Content.Client.MedicalScanner.UI 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"); text.Append($"{Loc.GetString("medical-scanner-window-entity-health-text", ("entityName", entity.Name))}\n");
// Show the total damage var totalDamage = state.DamagePerType.Values.Sum();
var totalDamage = state.DamagePerTypeID.Values.Sum();
text.Append($"{Loc.GetString("medical-scanner-window-entity-damage-total-text", ("amount", totalDamage))}\n"); 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> shownTypes = new();
HashSet<string> shownTypeIDs = new();
// First show just the total damage and type breakdown for each damge group that is fully supported by that entitygroup. // Show the total damage and type breakdown for each damage group.
foreach (var (damageGroupID, damageAmount) in state.DamagePerSupportedGroupID) 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))}"); 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. // Show the damage for each type in that group.
// currently state has a dictionary mapping groupsIDs to damage, and typeIDs to damage, but does not know how types and groups are related.
// So use PrototypeManager.
var group = IoCManager.Resolve<IPrototypeManager>().Index<DamageGroupPrototype>(damageGroupID); var group = IoCManager.Resolve<IPrototypeManager>().Index<DamageGroupPrototype>(damageGroupID);
foreach (var type in group.DamageTypes) 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 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); shownTypes.Add(type);
text.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-text", ("damageType", type.ID), ("amount", typeAmount))}"); text.Append($"\n- {Loc.GetString("medical-scanner-window-damage-type-text", ("damageType", type), ("amount", typeAmount))}");
} }
else { 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'); 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(); _diagnostics.Text = text.ToString();
ScanButton.Disabled = state.IsScanned; 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 System.Threading.Tasks;
using Content.Server.Damage; using Content.Server.Damage;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes;
using Content.Shared.MobState; using Content.Shared.MobState;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -21,7 +21,7 @@ namespace Content.IntegrationTests.Tests.Commands
id: DamageableDummy id: DamageableDummy
components: components:
- type: Damageable - type: Damageable
damageContainer: biologicalDamageContainer damageContainer: Biological
- type: MobState - type: MobState
thresholds: thresholds:
0: !type:NormalMobState {} 0: !type:NormalMobState {}
@@ -47,15 +47,17 @@ namespace Content.IntegrationTests.Tests.Commands
var human = entityManager.SpawnEntity("DamageableDummy", MapCoordinates.Nullspace); var human = entityManager.SpawnEntity("DamageableDummy", MapCoordinates.Nullspace);
// Sanity check // Sanity check
Assert.True(human.TryGetComponent(out IDamageableComponent damageable)); Assert.True(human.TryGetComponent(out DamageableComponent damageable));
Assert.True(human.TryGetComponent(out IMobStateComponent mobState)); Assert.True(human.TryGetComponent(out IMobStateComponent mobState));
mobState.UpdateState(0);
Assert.That(mobState.IsAlive, Is.True); Assert.That(mobState.IsAlive, Is.True);
Assert.That(mobState.IsCritical, Is.False); Assert.That(mobState.IsCritical, Is.False);
Assert.That(mobState.IsDead, Is.False); Assert.That(mobState.IsDead, Is.False);
Assert.That(mobState.IsIncapacitated, Is.False); Assert.That(mobState.IsIncapacitated, Is.False);
// Kill the entity // 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 // Check that it is dead
Assert.That(mobState.IsAlive, Is.False); 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
@@ -11,75 +11,76 @@ namespace Content.IntegrationTests.Tests.Damageable
{ {
[TestFixture] [TestFixture]
[TestOf(typeof(DamageableComponent))] [TestOf(typeof(DamageableComponent))]
[TestOf(typeof(DamageableSystem))]
public class DamageableTest : ContentIntegrationTest public class DamageableTest : ContentIntegrationTest
{ {
private const string DamageableEntityId = "TestDamageableEntityId"; private const string Prototypes = @"
private const string Group1Id = "TestGroup1";
private const string Group2Id = "TestGroup2";
private const string Group3Id = "TestGroup3";
private string Prototypes = $@"
# Define some damage groups # Define some damage groups
- type: damageType - type: damageType
id: TestDamage11 id: TestDamage1
- type: damageType - type: damageType
id: TestDamage21 id: TestDamage2a
- type: damageType - type: damageType
id: TestDamage22 id: TestDamage2b
- type: damageType - type: damageType
id: TestDamage31 id: TestDamage3a
- type: damageType - type: damageType
id: TestDamage32 id: TestDamage3b
- type: damageType - type: damageType
id: TestDamage33 id: TestDamage3c
# Define damage Groups with 1,2,3 damage types # Define damage Groups with 1,2,3 damage types
- type: damageGroup - type: damageGroup
id: {Group1Id} id: TestGroup1
damageTypes: damageTypes:
- TestDamage11 - TestDamage1
- type: damageGroup - type: damageGroup
id: {Group2Id} id: TestGroup2
damageTypes: damageTypes:
- TestDamage21 - TestDamage2a
- TestDamage22 - TestDamage2b
- type: damageGroup - type: damageGroup
id: {Group3Id} id: TestGroup3
damageTypes: damageTypes:
- TestDamage31 - TestDamage3a
- TestDamage32 - TestDamage3b
- TestDamage33 - TestDamage3c
# we want to test a container that supports only full groups - type: resistanceSet
# we will also give full support for group 2 IMPLICITLY by specifying all of its members are supported. id: testResistances
# this space is intentionally left blank
# This container should not support TestDamage1 or TestDamage2b
- type: damageContainer - type: damageContainer
id: testSomeDamageContainer id: testDamageContainer
defaultResistanceSet: testResistances
supportedGroups: supportedGroups:
- {Group3Id} - TestGroup3
supportedTypes: supportedTypes:
- TestDamage21 - TestDamage2a
- TestDamage22
- type: entity - type: entity
id: {DamageableEntityId} id: TestDamageableEntityId
name: {DamageableEntityId} name: TestDamageableEntityId
components: components:
- type: Damageable - type: Damageable
damageContainer: testSomeDamageContainer damageContainer: testDamageContainer
"; ";
/// <summary> // public bool & function to determine whether dealing damage resulted in actual damage change
/// Test a standard damageable components public bool DamageChanged = false;
/// </summary> public void DamageChangedListener(EntityUid _, DamageableComponent comp, DamageChangedEvent args)
/// <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. DamageChanged = true;
/// </remarks> }
[Test] [Test]
public async Task TestDamageableComponents() public async Task TestDamageableComponents()
{ {
@@ -93,360 +94,150 @@ namespace Content.IntegrationTests.Tests.Damageable
var sEntityManager = server.ResolveDependency<IEntityManager>(); var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>(); var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>(); var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDamageableEntity; sEntityManager.EventBus.SubscribeLocalEvent<DamageableComponent, DamageChangedEvent>(DamageChangedListener);
IDamageableComponent sDamageableComponent = null;
IEntity sDamageableEntity = null;
DamageableComponent sDamageableComponent = null;
DamageableSystem sDamageableSystem = null;
DamageGroupPrototype group1 = default!; DamageGroupPrototype group1 = default!;
DamageGroupPrototype group2 = default!; DamageGroupPrototype group2 = default!;
DamageGroupPrototype group3 = 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(() => await server.WaitPost(() =>
{ {
var mapId = sMapManager.NextMapId(); var mapId = sMapManager.NextMapId();
var coordinates = new MapCoordinates(0, 0, mapId); var coordinates = new MapCoordinates(0, 0, mapId);
sMapManager.CreateMap(mapId); sMapManager.CreateMap(mapId);
sDamageableEntity = sEntityManager.SpawnEntity(DamageableEntityId, coordinates); sDamageableEntity = sEntityManager.SpawnEntity("TestDamageableEntityId", coordinates);
sDamageableComponent = sDamageableEntity.GetComponent<IDamageableComponent>(); sDamageableComponent = sDamageableEntity.GetComponent<DamageableComponent>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
group1 = sPrototypeManager.Index<DamageGroupPrototype>(Group1Id); group1 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup1");
group2 = sPrototypeManager.Index<DamageGroupPrototype>(Group2Id); group2 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup2");
group3 = sPrototypeManager.Index<DamageGroupPrototype>(Group3Id); 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.WaitRunTicks(5);
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
// Check that the correct groups are supported by the container var uid = sDamageableEntity.Uid;
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);
// Check that the correct types are supported: // Check that the correct types are supported.
foreach (var group in sPrototypeManager.EnumeratePrototypes<DamageGroupPrototype>()) 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);
sDamageableSystem.TryChangeDamage(uid, damage, true);
Assert.That(DamageChanged);
DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal));
Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(damageToDeal));
foreach (var type in types)
{ {
foreach(var type in group.DamageTypes) Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage));
{ Assert.That(typeDamage, Is.EqualTo(damageToDeal / types.Count()));
if (sDamageableComponent.IsFullySupportedDamageGroup(group))
{
Assert.That(sDamageableComponent.IsSupportedDamageType(type), Is.True);
}
else
{
Assert.That(sDamageableComponent.IsSupportedDamageType(type), Is.False);
}
}
} }
// Heal
Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group1), Is.False); sDamageableSystem.TryChangeDamage(uid, -damage);
Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group2), Is.True); Assert.That(DamageChanged);
Assert.That(sDamageableComponent.IsFullySupportedDamageGroup(group3), Is.True); DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.Zero);
// Check that damage works properly if perfectly divisible among group members Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(0));
int damageToDeal, groupDamage, typeDamage; ; foreach (var type in types)
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
foreach (var damageGroup in sDamageableComponent.FullySupportedDamageGroups)
{ {
var types = damageGroup.DamageTypes; Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage));
Assert.That(typeDamage, Is.Zero);
// Damage
damageToDeal = types.Count() * 5;
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.True);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal));
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True);
Assert.That(groupDamage, Is.EqualTo(damageToDeal));
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True);
Assert.That(typeDamage, Is.EqualTo(damageToDeal / types.Count()));
}
// Heal
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, -damageToDeal, true), Is.True);
Assert.That(sDamageableComponent.TotalDamage, Is.Zero);
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True);
Assert.That(groupDamage, Is.Zero);
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True);
Assert.That(typeDamage, Is.Zero);
}
} }
// Check that damage works properly if it is NOT perfectly divisible among group members // Check that damage works properly if it is NOT perfectly divisible among group members
foreach (var damageGroup in sDamageableComponent.FullySupportedDamageGroups) types = group3.DamageTypes;
damageToDeal = types.Count() * 5 - 1;
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.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
sDamageableSystem.TryChangeDamage(uid, -damage);
Assert.That(DamageChanged);
DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.Zero);
Assert.That(sDamageableComponent.DamagePerGroup[group3.ID], Is.EqualTo(0));
foreach (var type in types)
{ {
var types = damageGroup.DamageTypes; Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type, out typeDamage));
Assert.That(typeDamage, Is.Zero);
// Damage
damageToDeal = types.Count() * 5 - 1;
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.True);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal));
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True);
Assert.That(groupDamage, Is.EqualTo(damageToDeal));
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True);
float targetDamage = ((float) damageToDeal) / types.Count();
Assert.That(typeDamage, Is.InRange(targetDamage - 1, targetDamage + 1));
}
// Heal
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, -damageToDeal, true), Is.True);
Assert.That(sDamageableComponent.TotalDamage, Is.Zero);
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.True);
Assert.That(groupDamage, Is.Zero);
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.True);
Assert.That(typeDamage, Is.Zero);
}
} }
// Test that unsupported groups return false when setting/getting damage (and don't change damage) // Test that unsupported groups return false when setting/getting damage (and don't change damage)
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
foreach (var damageGroup in sPrototypeManager.EnumeratePrototypes<DamageGroupPrototype>()) damage = new DamageSpecifier(group1, 10) + new DamageSpecifier(type2b, 10);
{ sDamageableSystem.TryChangeDamage(uid, damage, true);
if (sDamageableComponent.IsFullySupportedDamageGroup(damageGroup)) Assert.That(DamageChanged, Is.False);
{ Assert.That(sDamageableComponent.DamagePerGroup.TryGetValue(group1.ID, out groupDamage), Is.False);
continue; Assert.That(sDamageableComponent.Damage.DamageDict.TryGetValue(type1.ID, out typeDamage), Is.False);
}
Assert.That(sDamageableComponent.IsApplicableDamageGroup(damageGroup), Is.False);
var types = damageGroup.DamageTypes;
damageToDeal = types.Count() * 5;
foreach (var type in types)
{
Assert.That(sDamageableComponent.IsSupportedDamageType(type), Is.False);
}
;
Assert.That(sDamageableComponent.TryChangeDamage(damageGroup, damageToDeal, true), Is.False);
Assert.That(sDamageableComponent.TryGetDamage(damageGroup, out groupDamage), Is.False);
foreach (var type in types)
{
Assert.That(sDamageableComponent.TryChangeDamage(type, damageToDeal, true), Is.False);
Assert.That(sDamageableComponent.TryGetDamage(type, out typeDamage), Is.False);
}
}
// Did damage change?
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
// Test SetAll function
// Test total damage function sDamageableSystem.SetAllDamage(sDamageableComponent, 10);
damageToDeal = 10; Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(10 * sDamageableComponent.Damage.DamageDict.Count()));
sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
Assert.True(sDamageableComponent.TryChangeDamage(group3, damageToDeal, true));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(damageToDeal));
var totalTypeDamage = 0;
foreach (var damageType in sDamageableComponent.SupportedDamageTypes)
{
Assert.True(sDamageableComponent.TryGetDamage(damageType, out typeDamage));
Assert.That(typeDamage, Is.LessThanOrEqualTo(damageToDeal));
totalTypeDamage += typeDamage;
}
Assert.That(totalTypeDamage, Is.EqualTo(damageToDeal));
// Test healing all damage
Assert.That(sDamageableComponent.TrySetAllDamage(0));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
// Test preferential healing // Test 'wasted' healing
damageToDeal = 12; sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3a, 5));
var damageTypes = group3.DamageTypes.ToArray(); 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 // Test Over-Healing
Assert.True(sDamageableComponent.TryChangeDamage(damageTypes[0], 17)); sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -100));
Assert.True(sDamageableComponent.TryChangeDamage(damageTypes[1], 31)); Assert.That(DamageChanged);
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(48)); DamageChanged = false;
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
// 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 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 System.Threading.Tasks;
using Content.Server.Destructible.Thresholds.Triggers; using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -21,11 +21,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{ {
var server = StartServerDummyTicker(new ServerContentIntegrationOption var server = StartServerDummyTicker(new ServerContentIntegrationOption
{ {
ExtraPrototypes = Prototypes, ExtraPrototypes = Prototypes
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();
@@ -33,10 +29,12 @@ namespace Content.IntegrationTests.Tests.Destructible
var sEntityManager = server.ResolveDependency<IEntityManager>(); var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>(); var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>(); var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity; IEntity sDestructibleEntity = null;
IDamageableComponent sDamageableComponent = null; DamageableComponent sDamageableComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null; TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
DamageableSystem sDamageableSystem = null;
await server.WaitPost(() => await server.WaitPost(() =>
{ {
@@ -45,15 +43,16 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId); sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageGroupEntityId, coordinates); sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageGroupEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>(); sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>(); sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
}); });
await server.WaitRunTicks(5); await server.WaitRunTicks(5);
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
}); });
await server.WaitAssertion(() => await server.WaitAssertion(() =>
@@ -61,26 +60,29 @@ namespace Content.IntegrationTests.Tests.Destructible
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute"); var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute");
var burnDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBurn"); var burnDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBurn");
DamageSpecifier bruteDamage = new(bruteDamageGroup,5);
DamageSpecifier burnDamage = new(burnDamageGroup,5);
// Raise brute damage to 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 // No thresholds reached yet, the earliest one is at 10 damage
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage to 10 // 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 // No threshold reached, burn needs to be 10 as well
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 10 // 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 // 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 // Threshold brute 10 + burn 10
var msg = sThresholdListenerComponent.ThresholdsReached[0]; var msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var threshold = msg.Threshold; var threshold = msg.Threshold;
// Check that it matches the YAML prototype // 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[0]);
Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[1]); Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear(); sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Raise brute damage to 20 // Raise brute damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 20 // Raise burn damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, burnDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Lower brute damage to 0 // 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 // No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage back up to 10 // 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 // 10 brute + 10 burn threshold reached, brute 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 classes of damage to 0 // Heal both classes of damage to 0
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -10, true)); sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, -20, true));
// No new thresholds reached, healing should not trigger it // No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage to 10 // Raise brute damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 10 // 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 // 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 // Threshold brute 10 + burn 10
msg = sThresholdListenerComponent.ThresholdsReached[0]; msg = sTestThresholdListenerSystem.ThresholdsReached[0];
threshold = msg.Threshold; threshold = msg.Threshold;
// Check that it matches the YAML prototype // 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[0]);
Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[1]); Assert.IsInstanceOf<DamageGroupTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear(); sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Change triggers once to true // Change triggers once to true
threshold.TriggersOnce = true; threshold.TriggersOnce = true;
// Heal brute and burn back to 0 // Heal brute and burn back to 0
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, -10, true)); sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
Assert.True(sDamageableComponent.TryChangeDamage(burnDamageGroup, -10, true));
// No new thresholds reached from healing // No new thresholds reached from healing
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise brute damage to 10 // Raise brute damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bruteDamageGroup, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bruteDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise burn damage to 10 // 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 // 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 System.Threading.Tasks;
using Content.Server.Destructible.Thresholds.Triggers; using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -21,21 +21,19 @@ namespace Content.IntegrationTests.Tests.Destructible
{ {
var server = StartServerDummyTicker(new ServerContentIntegrationOption var server = StartServerDummyTicker(new ServerContentIntegrationOption
{ {
ExtraPrototypes = Prototypes, ExtraPrototypes = Prototypes
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();
var sEntityManager = server.ResolveDependency<IEntityManager>(); var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>(); var sMapManager = server.ResolveDependency<IMapManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity; IEntity sDestructibleEntity = null;
IDamageableComponent sDamageableComponent = null; DamageableComponent sDamageableComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null; TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
DamageableSystem sDamageableSystem = null;
await server.WaitPost(() => await server.WaitPost(() =>
{ {
@@ -44,15 +42,16 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId); sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageTypeEntityId, coordinates); sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDamageTypeEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>(); sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>(); sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
}); });
await server.WaitRunTicks(5); await server.WaitRunTicks(5);
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
}); });
await server.WaitAssertion(() => await server.WaitAssertion(() =>
@@ -60,26 +59,29 @@ namespace Content.IntegrationTests.Tests.Destructible
var bluntDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("TestBlunt"); var bluntDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("TestBlunt");
var slashDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>("TestSlash"); 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 // 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 // No thresholds reached yet, the earliest one is at 10 damage
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage to 10 // 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 // No threshold reached, slash needs to be 10 as well
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 10 // 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 // 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 // Threshold blunt 10 + slash 10
var msg = sThresholdListenerComponent.ThresholdsReached[0]; var msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var threshold = msg.Threshold; var threshold = msg.Threshold;
// Check that it matches the YAML prototype // 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[0]);
Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[1]); Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear(); sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Raise blunt damage to 20 // Raise blunt damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 20 // Raise slash damage to 20
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Lower blunt damage to 0 // 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 // No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage back up to 10 // 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 // 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 // Heal both types of damage to 0
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * -2, true);
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, -20, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * -4, true);
// No new thresholds reached, healing should not trigger it // No new thresholds reached, healing should not trigger it
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage to 10 // Raise blunt damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 10 // 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 // 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 // Threshold blunt 10 + slash 10
msg = sThresholdListenerComponent.ThresholdsReached[0]; msg = sTestThresholdListenerSystem.ThresholdsReached[0];
threshold = msg.Threshold; threshold = msg.Threshold;
// Check that it matches the YAML prototype // 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[0]);
Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[1]); Assert.IsInstanceOf<DamageTypeTrigger>(trigger.Triggers[1]);
sThresholdListenerComponent.ThresholdsReached.Clear(); sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Change triggers once to true // Change triggers once to true
threshold.TriggersOnce = true; threshold.TriggersOnce = true;
// Heal blunt and slash back to 0 // Heal blunt and slash back to 0
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, -10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * -2, true);
Assert.True(sDamageableComponent.TryChangeDamage(slashDamageType, -10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, slashDamage * -2, true);
// No new thresholds reached from healing // No new thresholds reached from healing
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise blunt damage to 10 // Raise blunt damage to 10
Assert.True(sDamageableComponent.TryChangeDamage(bluntDamageType, 10, true)); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage * 2, true);
// No new thresholds reached // No new thresholds reached
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Raise slash damage to 10 // 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 // 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;
using Content.Server.Destructible.Thresholds.Behaviors; using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -20,11 +20,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{ {
var server = StartServerDummyTicker(new ServerContentIntegrationOption var server = StartServerDummyTicker(new ServerContentIntegrationOption
{ {
ExtraPrototypes = Prototypes, ExtraPrototypes = Prototypes
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();
@@ -32,10 +28,11 @@ namespace Content.IntegrationTests.Tests.Destructible
var sEntityManager = server.ResolveDependency<IEntityManager>(); var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>(); var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>(); var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity = null; IEntity sDestructibleEntity = null;
IDamageableComponent sDamageableComponent = null; DamageableComponent sDamageableComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null; TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
await server.WaitPost(() => await server.WaitPost(() =>
{ {
@@ -44,23 +41,24 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId); sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDestructionEntityId, coordinates); sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleDestructionEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>(); sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>(); sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
}); });
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
var coordinates = sDestructibleEntity.Transform.Coordinates; var coordinates = sDestructibleEntity.Transform.Coordinates;
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute"); var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute");
DamageSpecifier bruteDamage = new(bruteDamageGroup,50);
Assert.DoesNotThrow(() => 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.Triggered, Is.True);
Assert.That(threshold.Behaviors.Count, Is.EqualTo(3)); Assert.That(threshold.Behaviors.Count, Is.EqualTo(3));

View File

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

View File

@@ -5,7 +5,7 @@ using Content.Server.Destructible.Thresholds;
using Content.Server.Destructible.Thresholds.Behaviors; using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Server.Destructible.Thresholds.Triggers; using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -17,7 +17,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{ {
[TestFixture] [TestFixture]
[TestOf(typeof(DestructibleComponent))] [TestOf(typeof(DestructibleComponent))]
[TestOf(typeof(Threshold))] [TestOf(typeof(DamageThreshold))]
public class DestructibleThresholdActivationTest : ContentIntegrationTest public class DestructibleThresholdActivationTest : ContentIntegrationTest
{ {
[Test] [Test]
@@ -25,11 +25,7 @@ namespace Content.IntegrationTests.Tests.Destructible
{ {
var server = StartServerDummyTicker(new ServerContentIntegrationOption var server = StartServerDummyTicker(new ServerContentIntegrationOption
{ {
ExtraPrototypes = Prototypes, ExtraPrototypes = Prototypes
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IComponentFactory>().RegisterClass<TestThresholdListenerComponent>();
}
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();
@@ -37,11 +33,13 @@ namespace Content.IntegrationTests.Tests.Destructible
var sEntityManager = server.ResolveDependency<IEntityManager>(); var sEntityManager = server.ResolveDependency<IEntityManager>();
var sMapManager = server.ResolveDependency<IMapManager>(); var sMapManager = server.ResolveDependency<IMapManager>();
var sPrototypeManager = server.ResolveDependency<IPrototypeManager>(); var sPrototypeManager = server.ResolveDependency<IPrototypeManager>();
var sEntitySystemManager = server.ResolveDependency<IEntitySystemManager>();
IEntity sDestructibleEntity; IEntity sDestructibleEntity = null; ;
IDamageableComponent sDamageableComponent = null; DamageableComponent sDamageableComponent = null;
DestructibleComponent sDestructibleComponent = null; DestructibleComponent sDestructibleComponent = null;
TestThresholdListenerComponent sThresholdListenerComponent = null; TestDestructibleListenerSystem sTestThresholdListenerSystem = null;
DamageableSystem sDamageableSystem = null;
await server.WaitPost(() => await server.WaitPost(() =>
{ {
@@ -50,34 +48,35 @@ namespace Content.IntegrationTests.Tests.Destructible
sMapManager.CreateMap(mapId); sMapManager.CreateMap(mapId);
sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleEntityId, coordinates); sDestructibleEntity = sEntityManager.SpawnEntity(DestructibleEntityId, coordinates);
sDamageableComponent = sDestructibleEntity.GetComponent<IDamageableComponent>(); sDamageableComponent = sDestructibleEntity.GetComponent<DamageableComponent>();
sDestructibleComponent = sDestructibleEntity.GetComponent<DestructibleComponent>(); sDestructibleComponent = sDestructibleEntity.GetComponent<DestructibleComponent>();
sThresholdListenerComponent = sDestructibleEntity.GetComponent<TestThresholdListenerComponent>(); sTestThresholdListenerSystem = sEntitySystemManager.GetEntitySystem<TestDestructibleListenerSystem>();
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
}); });
await server.WaitRunTicks(5); await server.WaitRunTicks(5);
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
}); });
await server.WaitAssertion(() => 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 // 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 // Only one threshold reached, 20
Assert.That(sThresholdListenerComponent.ThresholdsReached.Count, Is.EqualTo(1)); Assert.That(sTestThresholdListenerSystem.ThresholdsReached.Count, Is.EqualTo(1));
// Threshold 20 // Threshold 20
var msg = sThresholdListenerComponent.ThresholdsReached[0]; var msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var threshold = msg.Threshold; var threshold = msg.Threshold;
// Check that it matches the YAML prototype // Check that it matches the YAML prototype
@@ -85,15 +84,15 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.NotNull(threshold.Trigger); Assert.NotNull(threshold.Trigger);
Assert.That(threshold.Triggered, Is.True); 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 // 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 // Threshold 50
msg = sThresholdListenerComponent.ThresholdsReached[0]; msg = sTestThresholdListenerSystem.ThresholdsReached[0];
threshold = msg.Threshold; threshold = msg.Threshold;
// Check that it matches the YAML prototype // Check that it matches the YAML prototype
@@ -113,50 +112,50 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.NotNull(threshold.Trigger); Assert.NotNull(threshold.Trigger);
Assert.That(threshold.Triggered, Is.True); Assert.That(threshold.Triggered, Is.True);
sThresholdListenerComponent.ThresholdsReached.Clear(); sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Damage for 50 again, up to 100 now // 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 // No thresholds reached as they weren't healed below the trigger amount
Assert.IsEmpty(sThresholdListenerComponent.ThresholdsReached); Assert.IsEmpty(sTestThresholdListenerSystem.ThresholdsReached);
// Set damage to 0 // Set damage to 0
sDamageableComponent.TrySetAllDamage(0); sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// Damage for 100, up to 100 // 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 // 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 // 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 // Thresholds don't work backwards
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty); Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Damage for 10, up to 70 // 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 // 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 // Heal by 30, down to 40
sDamageableComponent.TryChangeDamage(bluntDamageType, -30, true); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*-3, true);
// Thresholds don't work backwards // Thresholds don't work backwards
Assert.That(sThresholdListenerComponent.ThresholdsReached, Is.Empty); Assert.That(sTestThresholdListenerSystem.ThresholdsReached, Is.Empty);
// Damage up to 50 again // 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 // 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; threshold = msg.Threshold;
// Check that it matches the YAML prototype // Check that it matches the YAML prototype
@@ -178,22 +177,22 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(threshold.Triggered, Is.True); Assert.That(threshold.Triggered, Is.True);
// Reset thresholds reached // Reset thresholds reached
sThresholdListenerComponent.ThresholdsReached.Clear(); sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal all damage // Heal all damage
sDamageableComponent.TrySetAllDamage(0); sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// Damage up to 50 // Damage up to 50
sDamageableComponent.TryChangeDamage(bluntDamageType, 50, true); sDamageableSystem.TryChangeDamage(sDestructibleEntity.Uid, bluntDamage*5, true);
// Check that the total damage matches // Check that the total damage matches
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50)); Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// Both thresholds should have triggered // 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) // Verify the first one, should be the lowest one (20)
msg = sThresholdListenerComponent.ThresholdsReached[0]; msg = sTestThresholdListenerSystem.ThresholdsReached[0];
var trigger = (DamageTrigger) msg.Threshold.Trigger; var trigger = (DamageTrigger) msg.Threshold.Trigger;
Assert.NotNull(trigger); Assert.NotNull(trigger);
Assert.That(trigger.Damage, Is.EqualTo(20)); Assert.That(trigger.Damage, Is.EqualTo(20));
@@ -204,7 +203,7 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(threshold.Behaviors, Is.Empty); Assert.That(threshold.Behaviors, Is.Empty);
// Verify the second one, should be the highest one (50) // Verify the second one, should be the highest one (50)
msg = sThresholdListenerComponent.ThresholdsReached[1]; msg = sTestThresholdListenerSystem.ThresholdsReached[1];
trigger = (DamageTrigger) msg.Threshold.Trigger; trigger = (DamageTrigger) msg.Threshold.Trigger;
Assert.NotNull(trigger); Assert.NotNull(trigger);
Assert.That(trigger.Damage, Is.EqualTo(50)); Assert.That(trigger.Damage, Is.EqualTo(50));
@@ -229,10 +228,10 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(threshold.Triggered, Is.True); Assert.That(threshold.Triggered, Is.True);
// Reset thresholds reached // Reset thresholds reached
sThresholdListenerComponent.ThresholdsReached.Clear(); sTestThresholdListenerSystem.ThresholdsReached.Clear();
// Heal the entity completely // Heal the entity completely
sDamageableComponent.TrySetAllDamage(0); sDamageableSystem.SetAllDamage(sDamageableComponent, 0);
// Check that the entity has 0 damage // Check that the entity has 0 damage
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0)); Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(0));
@@ -245,13 +244,13 @@ namespace Content.IntegrationTests.Tests.Destructible
} }
// Damage the entity up to 50 damage again // 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 // Check that the total damage matches
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50)); 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 // 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 // Set both thresholds to trigger multiple times
foreach (var destructibleThreshold in sDestructibleComponent.Thresholds) foreach (var destructibleThreshold in sDestructibleComponent.Thresholds)
@@ -264,7 +263,7 @@ namespace Content.IntegrationTests.Tests.Destructible
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50)); Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(50));
// They shouldn't have been triggered by changing TriggersOnce // 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Disposal.Tube.Components; using Content.Server.Disposal.Tube.Components;
using Content.Server.Disposal.Unit.Components; using Content.Server.Disposal.Unit.Components;
using Content.Server.Disposal.Unit.EntitySystems; using Content.Server.Disposal.Unit.EntitySystems;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Coordinates;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics; using Robust.Shared.Timing;
namespace Content.IntegrationTests.Tests.Disposal namespace Content.IntegrationTests.Tests.Disposal
{ {
@@ -19,13 +20,13 @@ namespace Content.IntegrationTests.Tests.Disposal
[TestOf(typeof(DisposalUnitComponent))] [TestOf(typeof(DisposalUnitComponent))]
public class DisposalUnitTest : ContentIntegrationTest 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) foreach (var entity in entities)
{ {
var insertTask = unit.TryInsert(entity);
Assert.That(EntitySystem.Get<DisposalUnitSystem>().CanInsert(unit, entity), Is.EqualTo(result)); 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)); Assert.That(task.Result, Is.EqualTo(result));
if (result) if (result)
@@ -34,7 +35,9 @@ namespace Content.IntegrationTests.Tests.Disposal
Assert.That(entity.Transform.Parent, Is.EqualTo(unit.Owner.Transform)); 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) 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); UnitContains(unit, result, entities);
} }
@@ -68,7 +71,9 @@ namespace Content.IntegrationTests.Tests.Disposal
- type: Body - type: Body
- type: MobState - type: MobState
- type: Damageable - type: Damageable
damagePrototype: biologicalDamageContainer damageContainer: Biological
- type: Physics
bodyType: KinematicController
- type: entity - type: entity
name: WrenchDummy name: WrenchDummy
@@ -78,6 +83,8 @@ namespace Content.IntegrationTests.Tests.Disposal
- type: Tool - type: Tool
qualities: qualities:
- Anchoring - Anchoring
- type: Physics
bodyType: Dynamic
- type: entity - type: entity
name: DisposalUnitDummy name: DisposalUnitDummy
@@ -94,44 +101,78 @@ namespace Content.IntegrationTests.Tests.Disposal
id: DisposalTrunkDummy id: DisposalTrunkDummy
components: components:
- type: DisposalEntry - type: DisposalEntry
- type: Transform
anchored: true
"; ";
[Test] [Test]
public async Task Test() public async Task Test()
{ {
var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes}; var options = new ServerIntegrationOptions { ExtraPrototypes = Prototypes };
var server = StartServerDummyTicker(options); var server = StartServerDummyTicker(options);
await server.WaitIdleAsync();
IEntity human; IEntity human = default!;
IEntity wrench; IEntity wrench = default!;
DisposalUnitComponent unit; 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 // Spawn the entities
human = entityManager.SpawnEntity("HumanDummy", MapCoordinates.Nullspace); human = entityManager.SpawnEntity("HumanDummy", coordinates);
wrench = entityManager.SpawnEntity("WrenchDummy", MapCoordinates.Nullspace); wrench = entityManager.SpawnEntity("WrenchDummy", coordinates);
var disposalUnit = entityManager.SpawnEntity("DisposalUnitDummy", MapCoordinates.Nullspace); disposalUnit = entityManager.SpawnEntity("DisposalUnitDummy", coordinates);
var disposalTrunk = entityManager.SpawnEntity("DisposalTrunkDummy", disposalUnit.Transform.MapPosition); 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 // Test for components existing
Assert.True(disposalUnit.TryGetComponent(out unit!)); Assert.True(disposalUnit.TryGetComponent(out unit!));
Assert.True(disposalTrunk.HasComponent<DisposalEntryComponent>()); Assert.True(disposalTrunk.HasComponent<DisposalEntryComponent>());
// Can't insert, unanchored and unpowered // Can't insert, unanchored and unpowered
var physics = disposalUnit.GetComponent<IPhysBody>(); unit.Owner.Transform.Anchored = false;
physics.BodyType = BodyType.Dynamic;
Assert.False(unit.Owner.Transform.Anchored);
UnitInsertContains(unit, false, human, wrench, disposalUnit, disposalTrunk); UnitInsertContains(unit, false, human, wrench, disposalUnit, disposalTrunk);
});
await server.WaitAssertion(() =>
{
// Anchor the disposal unit // Anchor the disposal unit
physics.BodyType = BodyType.Static; unit.Owner.Transform.Anchored = true;
// No power // No power
Assert.False(unit.Powered); Assert.False(unit.Powered);
@@ -141,19 +182,28 @@ namespace Content.IntegrationTests.Tests.Disposal
// Can insert mobs and items // Can insert mobs and items
UnitInsertContains(unit, true, human, wrench); UnitInsertContains(unit, true, human, wrench);
});
await server.WaitAssertion(() =>
{
// Move the disposal trunk away // Move the disposal trunk away
disposalTrunk.Transform.WorldPosition += (1, 0); disposalTrunk.Transform.WorldPosition += (1, 0);
// Fail to flush with a mob and an item // Fail to flush with a mob and an item
Flush(unit, false, human, wrench); Flush(unit, false, human, wrench);
});
await server.WaitAssertion(() =>
{
// Move the disposal trunk back // Move the disposal trunk back
disposalTrunk.Transform.WorldPosition -= (1, 0); disposalTrunk.Transform.WorldPosition -= (1, 0);
// Fail to flush with a mob and an item, no power // Fail to flush with a mob and an item, no power
Flush(unit, false, human, wrench); Flush(unit, false, human, wrench);
});
await server.WaitAssertion(() =>
{
// Remove power need // Remove power need
Assert.True(disposalUnit.TryGetComponent(out ApcPowerReceiverComponent? power)); Assert.True(disposalUnit.TryGetComponent(out ApcPowerReceiverComponent? power));
power!.NeedsPower = false; power!.NeedsPower = false;
@@ -161,12 +211,13 @@ namespace Content.IntegrationTests.Tests.Disposal
// Flush with a mob and an item // Flush with a mob and an item
Flush(unit, true, human, wrench); Flush(unit, true, human, wrench);
});
await server.WaitAssertion(() =>
{
// Re-pressurizing // Re-pressurizing
Flush(unit, false); 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 // 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.Server.AI.WorldState.States;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
namespace Content.Server.AI.Utility.Considerations.Combat namespace Content.Server.AI.Utility.Considerations.Combat
{ {
@@ -10,7 +10,7 @@ namespace Content.Server.AI.Utility.Considerations.Combat
{ {
var target = context.GetState<TargetEntityState>().GetValue(); 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; return 0.0f;
} }

View File

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

View File

@@ -5,11 +5,8 @@ using Content.Server.Pressure;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.Atmos.Components namespace Content.Server.Atmos.Components
@@ -22,21 +19,14 @@ namespace Content.Server.Atmos.Components
{ {
public override string Name => "Barotrauma"; public override string Name => "Barotrauma";
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("damageType")] private readonly string _damageTypeID = "Blunt";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update(float airPressure) public void Update(float airPressure)
{ {
if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) return; if (!Owner.HasComponent<DamageableComponent>()) return;
var status = Owner.GetComponentOrNull<ServerAlertsComponent>(); var status = Owner.GetComponentOrNull<ServerAlertsComponent>();
var highPressureMultiplier = 1f; var highPressureMultiplier = 1f;
@@ -59,7 +49,7 @@ namespace Content.Server.Atmos.Components
goto default; goto default;
// Deal damage and ignore resistances. Resistance to pressure damage should be done via pressure protection gear. // 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; if (status == null) break;
@@ -79,10 +69,10 @@ namespace Content.Server.Atmos.Components
if(pressure < Atmospherics.WarningHighPressure) if(pressure < Atmospherics.WarningHighPressure)
goto default; 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. // 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; if (status == null) break;

View File

@@ -10,7 +10,6 @@ using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Notification.Managers; using Content.Shared.Notification.Managers;
using Content.Shared.Temperature; using Content.Shared.Temperature;
@@ -20,8 +19,6 @@ using Robust.Shared.Localization;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
namespace Content.Server.Atmos.Components namespace Content.Server.Atmos.Components
{ {
@@ -45,17 +42,9 @@ namespace Content.Server.Atmos.Components
[DataField("canResistFire")] [DataField("canResistFire")]
public bool CanResistFire { get; private set; } = false; public bool CanResistFire { get; private set; } = false;
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("damageType")]
private readonly string _damageTypeID = "Heat"!;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public void Extinguish() public void Extinguish()
{ {
@@ -102,12 +91,9 @@ namespace Content.Server.Atmos.Components
temp.ReceiveHeat(200 * FireStacks); temp.ReceiveHeat(200 * FireStacks);
} }
if (Owner.TryGetComponent(out IDamageableComponent? damageable)) // TODO ATMOS Fire resistance from armor
{ var damageScale = Math.Min((int) (FireStacks * 2.5f), 10);
// TODO ATMOS Fire resistance from armor EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * damageScale);
var damage = Math.Min((int) (FireStacks * 2.5f), 10);
damageable.TryChangeDamage(DamageType, damage, false);
}
AdjustFireStacks(-0.1f * (_resisting ? 10f : 1f)); 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.Server.Body.Circulatory;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Body.Mechanism; using Content.Shared.Body.Mechanism;

View File

@@ -12,9 +12,6 @@ using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
using Content.Shared.MobState; using Content.Shared.MobState;
using Content.Shared.Notification.Managers; using Content.Shared.Notification.Managers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -92,21 +89,13 @@ namespace Content.Server.Body.Respiratory
[ViewVariables] public bool Suffocating { get; private set; } [ViewVariables] public bool Suffocating { get; private set; }
[ViewVariables(VVAccess.ReadWrite)] [DataField("suffocationDamage")] private int _damage = 1; [DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)] [DataField("suffocationDamageRecovery")] private int _damageRecovery = 1;
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported.
// Also remove Initialize override, if no longer needed.
[DataField("damageType")]
private readonly string _damageTypeID = "Asphyxiation"!;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize()
{ [DataField("damageRecovery", required: true)]
base.Initialize(); [ViewVariables(VVAccess.ReadWrite)]
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID); public DamageSpecifier DamageRecovery = default!;
}
private Dictionary<Gas, float> NeedsAndDeficit(float frameTime) private Dictionary<Gas, float> NeedsAndDeficit(float frameTime)
{ {
@@ -358,27 +347,19 @@ namespace Content.Server.Body.Respiratory
alertsComponent.ShowAlert(AlertType.LowOxygen); alertsComponent.ShowAlert(AlertType.LowOxygen);
} }
if (!Owner.TryGetComponent(out IDamageableComponent? damageable)) EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage);
{
return;
}
damageable.TryChangeDamage(DamageType, _damage, false);
} }
private void StopSuffocation() private void StopSuffocation()
{ {
Suffocating = false; Suffocating = false;
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{
damageable.TryChangeDamage(DamageType, -_damageRecovery, false);
}
if (Owner.TryGetComponent(out ServerAlertsComponent? alertsComponent)) if (Owner.TryGetComponent(out ServerAlertsComponent? alertsComponent))
{ {
alertsComponent.ClearAlert(AlertType.LowOxygen); alertsComponent.ClearAlert(AlertType.LowOxygen);
} }
EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, DamageRecovery);
} }
public GasMixture Clean(BloodstreamComponent bloodstream) public GasMixture Clean(BloodstreamComponent bloodstream)

View File

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

View File

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

View File

@@ -3,66 +3,23 @@ using Content.Shared.Chemistry.Reagent;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Content.Shared.Damage; 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 namespace Content.Server.Chemistry.ReagentEffects
{ {
/// <summary> /// <summary>
/// Default metabolism for medicine reagents. Attempts to find a DamageableComponent on the target, /// Default metabolism for medicine reagents.
/// and to update its damage values.
/// </summary> /// </summary>
public class HealthChange : ReagentEffect, ISerializationHooks public class HealthChange : ReagentEffect
{ {
/// <summary> /// <summary>
/// How much damage is changed when 1u of the reagent is metabolized. /// Damage to apply every metabolism cycle. Damage Ignores resistances.
/// </summary> /// </summary>
[DataField("healthChange")] [DataField("damage", required: true)]
public float AmountToChange { get; set; } = 1.0f; 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) public override void Metabolize(IEntity solutionEntity, Solution.ReagentQuantity amount)
{ {
if (solutionEntity.TryGetComponent(out IDamageableComponent? damageComponent)) EntitySystem.Get<DamageableSystem>().TryChangeDamage(solutionEntity.Uid, Damage, true);
{
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;
}
}
} }
} }
} }

View File

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

View File

@@ -3,8 +3,6 @@ using Content.Shared.Damage;
using Content.Shared.Sound; using Content.Shared.Sound;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.Damage.Components namespace Content.Server.Damage.Components
@@ -19,8 +17,6 @@ namespace Content.Server.Damage.Components
[DataField("minimumSpeed")] [DataField("minimumSpeed")]
public float MinimumSpeed { get; set; } = 20f; public float MinimumSpeed { get; set; } = 20f;
[DataField("baseDamage")]
public int BaseDamage { get; set; } = 5;
[DataField("factor")] [DataField("factor")]
public float Factor { get; set; } = 1f; public float Factor { get; set; } = 1f;
[DataField("soundHit", required: true)] [DataField("soundHit", required: true)]
@@ -36,16 +32,8 @@ namespace Content.Server.Damage.Components
internal TimeSpan LastHit = TimeSpan.Zero; internal TimeSpan LastHit = TimeSpan.Zero;
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("damageType")]
private readonly string _damageTypeID = "Blunt";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
} }
} }

View File

@@ -9,20 +9,13 @@ namespace Content.Server.Damage.Components
public sealed class DamageOnLandComponent : Component public sealed class DamageOnLandComponent : Component
{ {
public override string Name => "DamageOnLand"; public override string Name => "DamageOnLand";
[DataField("amount")]
[ViewVariables(VVAccess.ReadWrite)]
public int Amount = 1;
[DataField("ignoreResistances")] [DataField("ignoreResistances")]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public bool IgnoreResistances; public bool IgnoreResistances = false;
// 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";
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)] [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 System.Threading.Tasks;
using Content.Server.Tools.Components; using Content.Server.Tools.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Tool; using Content.Shared.Tool;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.Damage.Components namespace Content.Server.Damage.Components
@@ -19,28 +16,16 @@ namespace Content.Server.Damage.Components
public override string Name => "DamageOnToolInteract"; public override string Name => "DamageOnToolInteract";
[DataField("damage")]
protected int Damage;
[DataField("tools")] [DataField("tools")]
private List<ToolQuality> _tools = new(); private List<ToolQuality> _tools = new();
// TODO PROTOTYPE Replace these datafield variable with prototype references, once they are supported. [DataField("weldingDamage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("weldingDamageType")]
private readonly string _weldingDamageTypeID = "Heat";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype WeldingDamageType = default!; public DamageSpecifier WeldingDamage = default!;
[DataField("defaultDamageType")]
private readonly string _defaultDamageTypeID = "Blunt"; [DataField("defaultDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DefaultDamageType = default!; public DamageSpecifier DefaultDamage = default!;
protected override void Initialize()
{
base.Initialize();
WeldingDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_weldingDamageTypeID);
DefaultDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_defaultDamageTypeID);
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) 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 (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. 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); if (tool.HasQuality(toolQuality))
{
EntitySystem.Get<DamageableSystem>().TryChangeDamage(eventArgs.Target.Uid, DefaultDamage);
return true;
}
} }
} }
return false; return false;
} }
protected bool CallDamage(InteractUsingEventArgs eventArgs, ToolComponent tool)
{
if (!eventArgs.Target.TryGetComponent<IDamageableComponent>(out var damageable))
return false;
damageable.TryChangeDamage(tool.HasQuality(ToolQuality.Welding)
? WeldingDamageType
: DefaultDamageType,
Damage);
return true;
}
} }
} }

View File

@@ -3,8 +3,7 @@ using Content.Shared.Damage;
using Robust.Shared.Analyzers; using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes; using Robust.Shared.ViewVariables;
using Robust.Shared.IoC;
namespace Content.Server.Damage.Components namespace Content.Server.Damage.Components
{ {
@@ -14,21 +13,13 @@ namespace Content.Server.Damage.Components
{ {
public override string Name => "DamageOtherOnHit"; public override string Name => "DamageOtherOnHit";
[DataField("amount")]
public int Amount { get; } = 1;
[DataField("ignoreResistances")] [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.Components;
using Content.Server.Nutrition.EntitySystems; using Content.Server.Nutrition.EntitySystems;
using Content.Server.Stunnable.Components; using Content.Server.Stunnable.Components;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using Content.Shared.MobState; using Content.Shared.MobState;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Verbs; using Content.Shared.Verbs;
@@ -34,7 +34,7 @@ namespace Content.Server.Damage
if (user.TryGetComponent<ActorComponent>(out var player)) if (user.TryGetComponent<ActorComponent>(out var player))
{ {
if (!target.HasComponent<IDamageableComponent>() && !target.HasComponent<HungerComponent>() && if (!target.HasComponent<DamageableComponent>() && !target.HasComponent<HungerComponent>() &&
!target.HasComponent<ThirstComponent>()) !target.HasComponent<ThirstComponent>())
{ {
return; return;
@@ -59,14 +59,9 @@ namespace Content.Server.Damage
public static void PerformRejuvenate(IEntity target) public static void PerformRejuvenate(IEntity target)
{ {
if (target.TryGetComponent(out IDamageableComponent? damage)) if (target.TryGetComponent(out DamageableComponent? damageable))
{ {
damage.TrySetAllDamage(0); EntitySystem.Get<DamageableSystem>().SetAllDamage(damageable, 0);
}
if (target.TryGetComponent(out IMobStateComponent? mobState))
{
mobState.UpdateState(0);
} }
if (target.TryGetComponent(out HungerComponent? hunger)) if (target.TryGetComponent(out HungerComponent? hunger))

View File

@@ -1,7 +1,7 @@
using Content.Server.Damage.Components; using Content.Server.Damage.Components;
using Content.Server.Stunnable.Components; using Content.Server.Stunnable.Components;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -18,6 +18,7 @@ namespace Content.Server.Damage.Systems
{ {
[Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -27,7 +28,7 @@ namespace Content.Server.Damage.Systems
private void HandleCollide(EntityUid uid, DamageOnHighSpeedImpactComponent component, StartCollideEvent args) 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 otherBody = args.OtherFixture.Body.Owner;
var speed = args.OurFixture.Body.LinearVelocity.Length; var speed = args.OurFixture.Body.LinearVelocity.Length;
@@ -41,12 +42,11 @@ namespace Content.Server.Damage.Systems
component.LastHit = _gameTiming.CurTime; 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)) if (ComponentManager.TryGetComponent(uid, out StunnableComponent? stun) && _robustRandom.Prob(component.StunChance))
stun.Stun(component.StunSeconds); 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.Server.Damage.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Throwing; using Content.Shared.Throwing;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Content.Server.Damage.Systems namespace Content.Server.Damage.Systems
{ {
public sealed class DamageOnLandSystem : EntitySystem public sealed class DamageOnLandSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<DamageOnLandComponent, ComponentInit>(HandleInit);
SubscribeLocalEvent<DamageOnLandComponent, LandEvent>(DamageOnLand); 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) private void DamageOnLand(EntityUid uid, DamageOnLandComponent component, LandEvent args)
{ {
if (!ComponentManager.TryGetComponent<IDamageableComponent>(uid, out var damageable)) _damageableSystem.TryChangeDamage(uid, component.Damage, component.IgnoreResistances);
return;
damageable.TryChangeDamage(component.DamageType, component.Amount, component.IgnoreResistances);
} }
} }
} }

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.Destructible.Thresholds; using Content.Server.Destructible.Thresholds;
using Content.Shared.Damage; using Content.Shared.Damage;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -14,50 +14,11 @@ namespace Content.Server.Destructible
[RegisterComponent] [RegisterComponent]
public class DestructibleComponent : Component public class DestructibleComponent : Component
{ {
private DestructibleSystem _destructibleSystem = default!;
public override string Name => "Destructible"; public override string Name => "Destructible";
[ViewVariables] [ViewVariables]
[DataField("thresholds")] [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 JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -13,5 +15,44 @@ namespace Content.Server.Destructible
[Dependency] public readonly IRobustRandom Random = default!; [Dependency] public readonly IRobustRandom Random = default!;
[Dependency] public readonly AudioSystem AudioSystem = default!; [Dependency] public readonly AudioSystem AudioSystem = default!;
[Dependency] public readonly ActSystem ActSystem = 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.Behaviors;
using Content.Server.Destructible.Thresholds.Triggers; using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -9,7 +9,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Destructible.Thresholds namespace Content.Server.Destructible.Thresholds
{ {
[DataDefinition] [DataDefinition]
public class Threshold public class DamageThreshold
{ {
[DataField("behaviors")] [DataField("behaviors")]
private List<IThresholdBehavior> _behaviors = new(); private List<IThresholdBehavior> _behaviors = new();
@@ -49,7 +49,7 @@ namespace Content.Server.Destructible.Thresholds
/// </summary> /// </summary>
[ViewVariables] public IReadOnlyList<IThresholdBehavior> Behaviors => _behaviors; [ViewVariables] public IReadOnlyList<IThresholdBehavior> Behaviors => _behaviors;
public bool Reached(IDamageableComponent damageable, DestructibleSystem system) public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{ {
if (Trigger == null) if (Trigger == null)
{ {

View File

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

View File

@@ -1,9 +1,8 @@
using System; using System;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.IoC; using Content.Shared.Damage.Prototypes;
namespace Content.Server.Destructible.Thresholds.Triggers namespace Content.Server.Destructible.Thresholds.Triggers
{ {
@@ -15,12 +14,8 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataDefinition] [DataDefinition]
public class DamageGroupTrigger : IThresholdTrigger public class DamageGroupTrigger : IThresholdTrigger
{ {
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damageGroup", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<DamageGroupPrototype>))]
// While you're at it, maybe also combine damageGroup and damage into a dictionary, and allow it to test a sum public string DamageGroup { get; set; } = default!;
// of damage types?
[DataField("damageGroup", required: true)]
private string _damageGroupID { get; set; } = default!;
public DamageGroupPrototype DamageGroup => IoCManager.Resolve<IPrototypeManager>().Index<DamageGroupPrototype>(_damageGroupID);
/// <summary> /// <summary>
/// The amount of damage at which this threshold will trigger. /// The amount of damage at which this threshold will trigger.
@@ -28,15 +23,9 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataField("damage", required: true)] [DataField("damage", required: true)]
public int Damage { get; set; } = default!; public int Damage { get; set; } = default!;
public bool Reached(IDamageableComponent damageable, DestructibleSystem system) public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{ {
if (DamageGroup == null) return damageable.DamagePerGroup[DamageGroup] >= Damage;
{
return false;
}
return damageable.TryGetDamage(DamageGroup, out var damageReceived) &&
damageReceived >= Damage;
} }
} }
} }

View File

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

View File

@@ -1,9 +1,8 @@
using System; using System;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.IoC; using Content.Shared.Damage.Prototypes;
namespace Content.Server.Destructible.Thresholds.Triggers namespace Content.Server.Destructible.Thresholds.Triggers
{ {
@@ -15,24 +14,15 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataDefinition] [DataDefinition]
public class DamageTypeTrigger : IThresholdTrigger public class DamageTypeTrigger : IThresholdTrigger
{ {
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damageType", required:true, customTypeSerializer: typeof(PrototypeIdSerializer<DamageTypePrototype>))]
// While you're at it, maybe also combine damageGroup and damage into a dictionary, and allow it to test a sum public string DamageType { get; set; } = default!;
// of damage types?
[DataField("damageType", required:true)]
public string _damageTypeID { get; set; } = default!;
public DamageTypePrototype DamageType => IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
[DataField("damage", required: true)] [DataField("damage", required: true)]
public int Damage { get; set; } = default!; public int Damage { get; set; } = default!;
public bool Reached(IDamageableComponent damageable, DestructibleSystem system) public bool Reached(DamageableComponent damageable, DestructibleSystem system)
{ {
if (DamageType == null) return damageable.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) &&
{
return false;
}
return damageable.TryGetDamage(DamageType, out var damageReceived) &&
damageReceived >= Damage; damageReceived >= Damage;
} }
} }

View File

@@ -1,4 +1,4 @@
using Content.Shared.Damage.Components; using Content.Shared.Damage;
namespace Content.Server.Destructible.Thresholds.Triggers namespace Content.Server.Destructible.Thresholds.Triggers
{ {
@@ -13,6 +13,6 @@ namespace Content.Server.Destructible.Thresholds.Triggers
/// dependencies from, if any. /// dependencies from, if any.
/// </param> /// </param>
/// <returns>true if this trigger has been reached, false otherwise.</returns> /// <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 System.Collections.Generic;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Server.Destructible.Thresholds.Triggers namespace Content.Server.Destructible.Thresholds.Triggers
@@ -15,7 +15,7 @@ namespace Content.Server.Destructible.Thresholds.Triggers
[DataField("triggers")] [DataField("triggers")]
public List<IThresholdTrigger> Triggers { get; } = new(); 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) foreach (var trigger in Triggers)
{ {

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Shared.Damage;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Players; using Robust.Shared.Players;
@@ -40,35 +39,6 @@ namespace Content.Server.DoAfter
return new DoAfterComponentState(toAdd); 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) public void Add(DoAfter doAfter)
{ {
_doAfters.Add(doAfter, _runningIndex); _doAfters.Add(doAfter, _runningIndex);

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Damage;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -14,6 +15,28 @@ namespace Content.Server.DoAfter
private readonly List<DoAfter> _cancelled = new(); private readonly List<DoAfter> _cancelled = new();
private readonly List<DoAfter> _finished = 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) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);

View File

@@ -11,7 +11,6 @@ using Content.Server.Hands.Components;
using Content.Server.Stunnable.Components; using Content.Server.Stunnable.Components;
using Content.Server.Tools.Components; using Content.Server.Tools.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Sound; using Content.Shared.Sound;
@@ -22,16 +21,11 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Physics; 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.Player;
using Robust.Shared.Players; using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timing.Timer; using Timer = Robust.Shared.Timing.Timer;
using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
namespace Content.Server.Doors.Components namespace Content.Server.Doors.Components
{ {
@@ -47,17 +41,9 @@ namespace Content.Server.Doors.Components
[DataField("tryOpenDoorSound")] [DataField("tryOpenDoorSound")]
private SoundSpecifier _tryOpenDoorSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg"); private SoundSpecifier _tryOpenDoorSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg");
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("crushDamage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("damageType")]
private readonly string _damageTypeID = "Blunt";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier CrushDamage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public override DoorState State public override DoorState State
{ {
@@ -90,7 +76,6 @@ namespace Content.Server.Doors.Components
private CancellationTokenSource? _stateChangeCancelTokenSource; private CancellationTokenSource? _stateChangeCancelTokenSource;
private CancellationTokenSource? _autoCloseCancelTokenSource; private CancellationTokenSource? _autoCloseCancelTokenSource;
private const int DoorCrushDamage = 15;
private const float DoorStunTime = 5f; private const float DoorStunTime = 5f;
/// <summary> /// <summary>
@@ -537,7 +522,7 @@ namespace Content.Server.Doors.Components
foreach (var e in collidingentities) foreach (var e in collidingentities)
{ {
if (!e.Owner.TryGetComponent(out StunnableComponent? stun) if (!e.Owner.TryGetComponent(out StunnableComponent? stun)
|| !e.Owner.TryGetComponent(out IDamageableComponent? damage)) || !e.Owner.HasComponent<DamageableComponent>())
{ {
continue; continue;
} }
@@ -550,7 +535,8 @@ namespace Content.Server.Doors.Components
hitsomebody = true; hitsomebody = true;
CurrentlyCrushing.Add(e.Owner.Uid); CurrentlyCrushing.Add(e.Owner.Uid);
damage.TryChangeDamage(DamageType, DoorCrushDamage); EntitySystem.Get<DamageableSystem>().TryChangeDamage(e.Owner.Uid, CrushDamage);
stun.Paralyze(DoorStunTime); stun.Paralyze(DoorStunTime);
} }

View File

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

View File

@@ -15,7 +15,6 @@ using Content.Server.Traitor;
using Content.Server.TraitorDeathMatch.Components; using Content.Server.TraitorDeathMatch.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.MobState; using Content.Shared.MobState;
using Content.Shared.PDA; using Content.Shared.PDA;
@@ -28,6 +27,7 @@ using Robust.Shared.Localization;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Random; using Robust.Shared.Random;
using Content.Shared.Damage.Prototypes;
namespace Content.Server.GameTicking.Presets namespace Content.Server.GameTicking.Presets
{ {
@@ -194,13 +194,10 @@ namespace Content.Server.GameTicking.Presets
{ {
if (mobState.IsCritical()) if (mobState.IsCritical())
{ {
// TODO: This is copy/pasted from ghost code. Really, IDamageableComponent needs a method to reliably kill the target. // TODO BODY SYSTEM KILL
if (entity.TryGetComponent(out IDamageableComponent? damageable)) var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100);
{ EntitySystem.Get<DamageableSystem>().TryChangeDamage(entity.Uid, damage, true);
//todo: what if they dont breathe lol }
damageable.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), 100, true);
}
}
else if (!mobState.IsDead()) else if (!mobState.IsDead())
{ {
if (entity.HasComponent<HandsComponent>()) if (entity.HasComponent<HandsComponent>())

View File

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

View File

@@ -6,7 +6,6 @@ using Content.Server.Power.Components;
using Content.Server.Temperature.Components; using Content.Server.Temperature.Components;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Light; using Content.Shared.Light;
using Content.Shared.Notification.Managers; using Content.Shared.Notification.Managers;
@@ -16,7 +15,6 @@ using Robust.Shared.Audio;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -81,16 +79,13 @@ namespace Content.Server.Light.Components
[ViewVariables] private ContainerSlot _lightBulbContainer = default!; [ViewVariables] private ContainerSlot _lightBulbContainer = default!;
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
[DataField("damageType")]
private readonly string _damageTypeID = "Heat";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize() protected override void Initialize()
{ {
base.Initialize(); base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
_lightBulbContainer = Owner.EnsureContainer<ContainerSlot>("light_bulb"); _lightBulbContainer = Owner.EnsureContainer<ContainerSlot>("light_bulb");
} }
@@ -116,7 +111,7 @@ namespace Content.Server.Light.Components
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
{ {
if (!eventArgs.User.TryGetComponent(out IDamageableComponent? damageableComponent)) if (!eventArgs.User.HasComponent<DamageableComponent>())
{ {
Eject(); Eject();
return false; return false;
@@ -143,7 +138,7 @@ namespace Content.Server.Light.Components
void Burn() void Burn()
{ {
Owner.PopupMessage(eventArgs.User, Loc.GetString("powered-light-component-burn-hand")); 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); SoundSystem.Play(Filter.Pvs(Owner), _burnHandSound.GetSound(), Owner);
} }
@@ -285,17 +280,11 @@ namespace Content.Server.Light.Components
case PowerChangedMessage: case PowerChangedMessage:
UpdateLight(); UpdateLight();
break; 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) if (LightBulb == null || LightBulb.State == LightBulbState.Broken)
return; return;

View File

@@ -1,17 +1,19 @@
using System; using System;
using Content.Server.Ghost; using Content.Server.Ghost;
using Content.Server.Light.Components; using Content.Server.Light.Components;
using Content.Server.MachineLinking.Events;
using Content.Shared.Light; using Content.Shared.Light;
using Content.Shared.Damage;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Content.Server.Light.Components;
using Content.Server.MachineLinking.Events;
using Robust.Shared.GameObjects;
namespace Content.Server.Light.EntitySystems namespace Content.Server.Light.EntitySystems
{ {
/// <summary>
/// System for the PoweredLightComponent. Currently bare-bones, to handle events from the DamageableSystem
/// </summary>
public class PoweredLightSystem : EntitySystem public class PoweredLightSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -21,6 +23,20 @@ namespace Content.Server.Light.EntitySystems
base.Initialize(); base.Initialize();
SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo); SubscribeLocalEvent<PoweredLightComponent, GhostBooEvent>(OnGhostBoo);
SubscribeLocalEvent<PoweredLightComponent, SignalReceivedEvent>(OnSignalReceived); 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) private void OnGhostBoo(EntityUid uid, PoweredLightComponent light, GhostBooEvent args)

View File

@@ -1,16 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Stack; using Content.Server.Stack;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers; using Content.Shared.Interaction.Helpers;
using Content.Shared.Stacks; using Content.Shared.Stacks;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.Medical.Components namespace Content.Server.Medical.Components
@@ -20,12 +16,9 @@ namespace Content.Server.Medical.Components
{ {
public override string Name => "Healing"; public override string Name => "Healing";
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
// This also requires changing the dictionary type, and removing a _prototypeManager.Index() call.
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[DataField("heal", required: true )]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> Heal = new(); public DamageSpecifier Damage = default!;
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
{ {
@@ -34,7 +27,7 @@ namespace Content.Server.Medical.Components
return false; return false;
} }
if (!eventArgs.Target.TryGetComponent(out IDamageableComponent? damageable)) if (!eventArgs.Target.HasComponent<DamageableComponent>())
{ {
return true; return true;
} }
@@ -55,10 +48,7 @@ namespace Content.Server.Medical.Components
return true; return true;
} }
foreach (var (damageTypeID, amount) in Heal) EntitySystem.Get<DamageableSystem>().TryChangeDamage(eventArgs.Target.Uid, Damage, true);
{
damageable.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(damageTypeID), -amount, true);
}
return true; return true;
} }

View File

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

View File

@@ -1,7 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Weapon.Melee.Components; using Content.Server.Weapon.Melee.Components;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Mining; using Content.Shared.Mining;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -24,16 +23,9 @@ namespace Content.Server.Mining.Components
public override string Name => "AsteroidRock"; public override string Name => "AsteroidRock";
private static readonly string[] SpriteStates = {"0", "1", "2", "3", "4"}; 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() protected override void Initialize()
{ {
base.Initialize(); base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
if (Owner.TryGetComponent(out AppearanceComponent? appearance)) if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{ {
appearance.SetData(AsteroidRockVisuals.State, _random.Pick(SpriteStates)); appearance.SetData(AsteroidRockVisuals.State, _random.Pick(SpriteStates));
@@ -46,7 +38,7 @@ namespace Content.Server.Mining.Components
if (!item.TryGetComponent(out MeleeWeaponComponent? meleeWeaponComponent)) if (!item.TryGetComponent(out MeleeWeaponComponent? meleeWeaponComponent))
return false; return false;
Owner.GetComponent<IDamageableComponent>().TryChangeDamage(DamageType, meleeWeaponComponent.Damage); EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, meleeWeaponComponent.Damage);
if (!item.TryGetComponent(out PickaxeComponent? pickaxeComponent)) if (!item.TryGetComponent(out PickaxeComponent? pickaxeComponent))
return true; 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.Alert;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using Content.Shared.MobState; using Content.Shared.MobState;
using Content.Shared.MobState.State; using Content.Shared.MobState.State;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -13,7 +13,7 @@ namespace Content.Server.MobState.States
{ {
base.UpdateState(entity, threshold); base.UpdateState(entity, threshold);
if (!entity.TryGetComponent(out IDamageableComponent? damageable)) if (!entity.TryGetComponent(out DamageableComponent? damageable))
{ {
return; return;
} }

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using Content.Server.Alert; using Content.Server.Alert;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.MobState; using Content.Shared.MobState;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
@@ -14,7 +13,6 @@ using Robust.Shared.Players;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
using Robust.Shared.Prototypes;
namespace Content.Server.Nutrition.Components namespace Content.Server.Nutrition.Components
{ {
@@ -23,9 +21,7 @@ namespace Content.Server.Nutrition.Components
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
// TODO DAMAGE UNITS When damage units support decimals, get rid of this. private float _accumulatedFrameTime;
// See also _accumulatedDamage in ThirstComponent and HealthChange.
private float _accumulatedDamage;
// Base stuff // Base stuff
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
@@ -78,17 +74,9 @@ namespace Content.Server.Nutrition.Components
{ HungerThreshold.Starving, AlertType.Starving }, { HungerThreshold.Starving, AlertType.Starving },
}; };
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("damageType")]
private readonly string _damageTypeID = "Blunt"!;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public void HungerThresholdEffect(bool force = false) public void HungerThresholdEffect(bool force = false)
{ {
@@ -196,21 +184,17 @@ namespace Content.Server.Nutrition.Components
return; return;
// --> Current Hunger is below dead threshold // --> Current Hunger is below dead threshold
if (!Owner.TryGetComponent(out IDamageableComponent? damageable))
return;
if (!Owner.TryGetComponent(out IMobStateComponent? mobState)) if (!Owner.TryGetComponent(out IMobStateComponent? mobState))
return; return;
if (!mobState.IsDead()) if (!mobState.IsDead())
{ {
// --> But they are not dead yet. // --> But they are not dead yet.
var damage = 2 * frametime; _accumulatedFrameTime += frametime;
_accumulatedDamage += damage - ((int) damage); if (_accumulatedFrameTime >= 1)
damageable.TryChangeDamage(DamageType, (int) damage); {
if (_accumulatedDamage >= 1) { EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * (int) _accumulatedFrameTime, true);
_accumulatedDamage -= 1; _accumulatedFrameTime -= (int) _accumulatedFrameTime;
damageable.TryChangeDamage(DamageType, 1, true);
} }
} }
} }

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using Content.Server.Alert; using Content.Server.Alert;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.MobState; using Content.Shared.MobState;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
@@ -14,7 +13,6 @@ using Robust.Shared.Players;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
using Robust.Shared.Prototypes;
namespace Content.Server.Nutrition.Components namespace Content.Server.Nutrition.Components
{ {
@@ -23,9 +21,7 @@ namespace Content.Server.Nutrition.Components
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
// TODO DAMAGE UNITS When damage units support decimals, get rid of this. private float _accumulatedFrameTime;
// See also _accumulatedDamage in HungerComponent and HealthChange.
private float _accumulatedDamage;
// Base stuff // Base stuff
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
@@ -77,17 +73,9 @@ namespace Content.Server.Nutrition.Components
{ThirstThreshold.Parched, AlertType.Parched}, {ThirstThreshold.Parched, AlertType.Parched},
}; };
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("damageType")]
private readonly string _damageTypeID = "Blunt";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
public void ThirstThresholdEffect(bool force = false) public void ThirstThresholdEffect(bool force = false)
{ {
@@ -193,22 +181,17 @@ namespace Content.Server.Nutrition.Components
return; return;
// --> Current Hunger is below dead threshold // --> Current Hunger is below dead threshold
if (!Owner.TryGetComponent(out IDamageableComponent? damageable))
return;
if (!Owner.TryGetComponent(out IMobStateComponent? mobState)) if (!Owner.TryGetComponent(out IMobStateComponent? mobState))
return; return;
if (!mobState.IsDead()) if (!mobState.IsDead())
{ {
// --> But they are not dead yet. // --> But they are not dead yet.
var damage = 2 * frametime; _accumulatedFrameTime += frametime;
_accumulatedDamage += damage - ((int) damage); if (_accumulatedFrameTime >= 1)
damageable.TryChangeDamage(DamageType, (int) damage);
if (_accumulatedDamage >= 1)
{ {
_accumulatedDamage -= 1; EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, Damage * (int) _accumulatedFrameTime, true);
damageable.TryChangeDamage(DamageType, 1, true); _accumulatedFrameTime -= (int) _accumulatedFrameTime;
} }
} }
} }

View File

@@ -9,7 +9,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -27,18 +26,19 @@ namespace Content.Server.Projectiles.Components
public override string Name => "Hitscan"; public override string Name => "Hitscan";
public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask; public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask;
[DataField("layers")] //todo WithFormat.Flags<CollisionLayer>() [DataField("layers")] //todo WithFormat.Flags<CollisionLayer>()
private int _collisionMask = (int) CollisionGroup.Opaque; 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 _startTime;
private TimeSpan _deathTime; private TimeSpan _deathTime;
public float ColorModifier { get; set; } = 1.0f; public float ColorModifier { get; set; } = 1.0f;
[DataField("spriteName")] [DataField("spriteName")]
private string _spriteName = "Objects/Weapons/Guns/Projectiles/laser.png"; private string _spriteName = "Objects/Weapons/Guns/Projectiles/laser.png";
[DataField("muzzleFlash")] [DataField("muzzleFlash")]
private string? _muzzleFlash; private string? _muzzleFlash;
@@ -47,19 +47,6 @@ namespace Content.Server.Projectiles.Components
[DataField("soundHitWall")] [DataField("soundHitWall")]
private SoundSpecifier _soundHitWall = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg"); 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) public void FireEffects(IEntity user, float distance, Angle angle, IEntity? hitEntity = null)
{ {
var effectSystem = EntitySystem.Get<EffectSystem>(); var effectSystem = EntitySystem.Get<EffectSystem>();

View File

@@ -13,12 +13,9 @@ namespace Content.Server.Projectiles.Components
[ComponentReference(typeof(SharedProjectileComponent))] [ComponentReference(typeof(SharedProjectileComponent))]
public class ProjectileComponent : SharedProjectileComponent public class ProjectileComponent : SharedProjectileComponent
{ {
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required: true)]
// This also requires changing the dictionary type and modifying ProjectileSystem.cs, which uses it.
// While thats being done, also replace "damages" -> "damageTypes" For consistency.
[DataField("damages")]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public Dictionary<string, int> Damages { get; set; } = new(); public DamageSpecifier Damage = default!;
[DataField("deleteOnCollide")] [DataField("deleteOnCollide")]
public bool DeleteOnCollide { get; } = true; public bool DeleteOnCollide { get; } = true;

View File

@@ -1,22 +1,20 @@
using Content.Server.Camera; using Content.Server.Camera;
using Content.Server.Projectiles.Components; using Content.Server.Projectiles.Components;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.IoC;
using Content.Shared.Damage;
namespace Content.Server.Projectiles namespace Content.Server.Projectiles
{ {
[UsedImplicitly] [UsedImplicitly]
internal sealed class ProjectileSystem : EntitySystem internal sealed class ProjectileSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -50,16 +48,12 @@ namespace Content.Server.Projectiles
SoundSystem.Play(playerFilter, soundHit, coordinates); SoundSystem.Play(playerFilter, soundHit, coordinates);
} }
if (!otherEntity.Deleted && otherEntity.TryGetComponent(out IDamageableComponent? damage)) if (!otherEntity.Deleted)
{ {
EntityManager.TryGetEntity(component.Shooter, out var shooter); _damageableSystem.TryChangeDamage(otherEntity.Uid, component.Damage);
foreach (var (damageTypeID, amount) in component.Damages)
{
damage.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(damageTypeID), amount);
}
component.DamagedEntity = true; 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 // 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.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.Repairable namespace Content.Server.Repairable
{ {
[RegisterComponent] [RegisterComponent]
public class RepairableComponent : Component, IInteractUsing public class RepairableComponent : Component
{ {
public override string Name => "Repairable"; public override string Name => "Repairable";
[ViewVariables(VVAccess.ReadWrite)] [DataField("fuelCost")] [ViewVariables(VVAccess.ReadWrite)] [DataField("fuelCost")]
private int _fuelCost = 5; public int FuelCost = 5;
[ViewVariables(VVAccess.ReadWrite)] [DataField("doAfterDelay")] [ViewVariables(VVAccess.ReadWrite)] [DataField("doAfterDelay")]
private int _doAfterDelay = 1; public 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;
}
} }
} }

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.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Serialization.Manager.Attributes; 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. [DataField("coldDamage", required: true)]
// Also remove Initialize override, if no longer needed.
[DataField("coldDamageType")]
private readonly string _coldDamageTypeID = "Cold";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype ColdDamageType = default!; public DamageSpecifier ColdDamage = default!;
[DataField("hotDamageType")]
private readonly string _hotDamageTypeID = "Heat"; [DataField("heatDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype HotDamageType = default!; public DamageSpecifier HeatDamage = default!;
protected override void Initialize()
{
base.Initialize();
ColdDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_coldDamageTypeID);
HotDamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_hotDamageTypeID);
}
public void Update() 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) if (CurrentTemperature >= _heatDamageThreshold)
{ {
int tempDamage = (int) Math.Floor((CurrentTemperature - _heatDamageThreshold) * _tempDamageCoefficient); 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) else if (CurrentTemperature <= _coldDamageThreshold)
{ {
int tempDamage = (int) Math.Floor((_coldDamageThreshold - CurrentTemperature) * _tempDamageCoefficient); int tempDamage = (int) Math.Floor((_coldDamageThreshold - CurrentTemperature) * _tempDamageCoefficient);
component.TryChangeDamage(ColdDamageType, tempDamage, false); EntitySystem.Get<DamageableSystem>().TryChangeDamage(Owner.Uid, ColdDamage * tempDamage);
} }
} }
/// <summary> /// <summary>

View File

@@ -4,8 +4,6 @@ using Content.Shared.Sound;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
namespace Content.Server.Weapon.Melee.Components namespace Content.Server.Weapon.Melee.Components
{ {
@@ -46,10 +44,6 @@ namespace Content.Server.Weapon.Melee.Components
[DataField("range")] [DataField("range")]
public float Range { get; set; } = 1; public float Range { get; set; } = 1;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("damage")]
public int Damage { get; set; } = 5;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("clickAttackEffect")] [DataField("clickAttackEffect")]
public bool ClickAttackEffect { get; set; } = true; public bool ClickAttackEffect { get; set; } = true;
@@ -57,16 +51,8 @@ namespace Content.Server.Weapon.Melee.Components
public TimeSpan LastAttackTime; public TimeSpan LastAttackTime;
public TimeSpan CooldownEnd; public TimeSpan CooldownEnd;
// TODO PROTOTYPE Replace this datafield variable with prototype references, once they are supported. [DataField("damage", required:true)]
// Also remove Initialize override, if no longer needed.
[DataField("damageType")]
private readonly string _damageTypeID = "Blunt";
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DamageTypePrototype DamageType = default!; public DamageSpecifier Damage = default!;
protected override void Initialize()
{
base.Initialize();
DamageType = IoCManager.Resolve<IPrototypeManager>().Index<DamageTypePrototype>(_damageTypeID);
}
} }
} }

View File

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

View File

@@ -1,12 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Hands.Components; using Content.Server.Hands.Components;
using Content.Server.Items; using Content.Server.Items;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Projectiles.Components; using Content.Server.Projectiles.Components;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Damage;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Events;
using Content.Shared.Sound; using Content.Shared.Sound;
@@ -188,13 +186,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
{ {
if (energyRatio < 1.0) if (energyRatio < 1.0)
{ {
var newDamages = new Dictionary<string, int>(projectileComponent.Damages.Count); projectileComponent.Damage *= energyRatio;
foreach (var (damageType, damage) in projectileComponent.Damages)
{
newDamages.Add(damageType, (int) (damage * energyRatio));
}
projectileComponent.Damages = newDamages;
} }
} else if (entity.TryGetComponent(out HitscanComponent? hitscanComponent)) } 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.Projectiles.Components;
using Content.Server.Weapon.Ranged.Ammunition.Components; using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Damage.Components; using Content.Shared.Damage;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Sound; using Content.Shared.Sound;
@@ -395,13 +395,7 @@ namespace Content.Server.Weapon.Ranged.Barrels.Components
var result = rayCastResults[0]; var result = rayCastResults[0];
var distance = result.Distance; var distance = result.Distance;
hitscan.FireEffects(shooter, distance, angle, result.HitEntity); hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
EntitySystem.Get<DamageableSystem>().TryChangeDamage(result.HitEntity.Uid, hitscan.Damage);
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
} }
else else
{ {

View File

@@ -1,5 +1,4 @@
using System; using System;
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.CombatMode; using Content.Server.CombatMode;
using Content.Server.Hands.Components; using Content.Server.Hands.Components;
@@ -8,9 +7,7 @@ using Content.Server.Stunnable.Components;
using Content.Server.Weapon.Ranged.Barrels.Components; using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Interaction.Events;
using Content.Shared.Notification.Managers; using Content.Shared.Notification.Managers;
using Content.Shared.Sound; using Content.Shared.Sound;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
@@ -27,8 +24,6 @@ using Robust.Shared.Players;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
using Robust.Shared.Prototypes;
using System.Collections.Generic;
namespace Content.Server.Weapon.Ranged namespace Content.Server.Weapon.Ranged
{ {
@@ -57,17 +52,10 @@ namespace Content.Server.Weapon.Ranged
[DataField("clumsyWeaponShotSound")] [DataField("clumsyWeaponShotSound")]
private SoundSpecifier _clumsyWeaponShotSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg"); 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)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("clumsyDamage")] [DataField("clumsyDamage")]
public Dictionary<string, int> ClumsyDamage { get; set; } = new() public DamageSpecifier? ClumsyDamage;
{
{ "Blunt", 10 },
{ "Heat", 5 }
};
public Func<bool>? WeaponCanFireHandler; public Func<bool>? WeaponCanFireHandler;
public Func<IEntity, bool>? UserCanFireHandler; public Func<IEntity, bool>? UserCanFireHandler;
@@ -179,16 +167,10 @@ namespace Content.Server.Weapon.Ranged
_lastFireTime = curTime; _lastFireTime = curTime;
if (ClumsyCheck && ClumsyComponent.TryRollClumsy(user, ClumsyExplodeChance)) if (ClumsyCheck && ClumsyDamage != null && ClumsyComponent.TryRollClumsy(user, ClumsyExplodeChance))
{ {
//Wound them //Wound them
if (user.TryGetComponent(out IDamageableComponent? health)) EntitySystem.Get<DamageableSystem>().TryChangeDamage(user.Uid, ClumsyDamage);
{
foreach (KeyValuePair<string, int> damage in ClumsyDamage)
{
health.TryChangeDamage(_prototypeManager.Index<DamageTypePrototype>(damage.Key), damage.Value);
}
}
// Knock them down // Knock them down
if (user.TryGetComponent(out StunnableComponent? stun)) if (user.TryGetComponent(out StunnableComponent? stun))
@@ -197,14 +179,14 @@ namespace Content.Server.Weapon.Ranged
} }
// Apply salt to the wound ("Honk!") // Apply salt to the wound ("Honk!")
SoundSystem.Play( SoundSystem.Play(
Filter.Pvs(Owner), _clumsyWeaponHandlingSound.GetSound(), Filter.Pvs(Owner), _clumsyWeaponHandlingSound.GetSound(),
Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5)); Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5));
SoundSystem.Play( SoundSystem.Play(
Filter.Pvs(Owner), _clumsyWeaponShotSound.GetSound(), Filter.Pvs(Owner), _clumsyWeaponShotSound.GetSound(),
Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5)); Owner.Transform.Coordinates, AudioParams.Default.WithMaxDistance(5));
user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy")); user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy"));
Owner.Delete(); Owner.Delete();

View File

@@ -4,7 +4,6 @@ using Content.Server.Destructible.Thresholds.Triggers;
using Content.Server.Notification; using Content.Server.Notification;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Rounding; using Content.Shared.Rounding;
@@ -41,22 +40,7 @@ namespace Content.Server.Window
[DataField("knockSound")] [DataField("knockSound")]
private SoundSpecifier _knockSound = new SoundPathSpecifier("/Audio/Effects/glass_knock.ogg"); private SoundSpecifier _knockSound = new SoundPathSpecifier("/Audio/Effects/glass_knock.ogg");
public override void HandleMessage(ComponentMessage message, IComponent? component) public void UpdateVisuals(int currentDamage)
{
base.HandleMessage(message, component);
switch (message)
{
case DamageChangedMessage msg:
{
var current = msg.Damageable.TotalDamage;
UpdateVisuals(current);
break;
}
}
}
private void UpdateVisuals(int currentDamage)
{ {
if (Owner.TryGetComponent(out AppearanceComponent? appearance) && if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
Owner.TryGetComponent(out DestructibleComponent? destructible)) Owner.TryGetComponent(out DestructibleComponent? destructible))
@@ -75,7 +59,7 @@ namespace Content.Server.Window
void IExamine.Examine(FormattedMessage message, bool inDetailsRange) void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{ {
if (!Owner.TryGetComponent(out IDamageableComponent? damageable) || if (!Owner.TryGetComponent(out DamageableComponent? damageable) ||
!Owner.TryGetComponent(out DestructibleComponent? destructible)) !Owner.TryGetComponent(out DestructibleComponent? destructible))
{ {
return; 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.Slot;
using Content.Shared.Body.Template; using Content.Shared.Body.Template;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components; using Content.Shared.Damage.Prototypes;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Standing; using Content.Shared.Standing;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -73,23 +73,6 @@ namespace Content.Shared.Body.Components
public SharedBodyPartComponent? CenterPart => CenterSlot?.Part; 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() protected override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -98,7 +81,6 @@ namespace Content.Shared.Body.Components
// TODO BODY Move to template or somewhere else // TODO BODY Move to template or somewhere else
if (TemplateId != null) if (TemplateId != null)
{ {
VitalPartsRemovedDamageType = _prototypeManager.Index<DamageTypePrototype>(_vitalPartsRemovedDamageTypeID);
var template = _prototypeManager.Index<BodyTemplatePrototype>(TemplateId); var template = _prototypeManager.Index<BodyTemplatePrototype>(TemplateId);
foreach (var (id, partType) in template.Slots) foreach (var (id, partType) in template.Slots)
@@ -207,13 +189,11 @@ namespace Content.Shared.Body.Components
EntitySystem.Get<StandingStateSystem>().Down(Owner); EntitySystem.Get<StandingStateSystem>().Down(Owner);
} }
// creadth: immediately kill entity if last vital part removed if (part.IsVital && SlotParts.Count(x => x.Value.PartType == part.PartType) == 0)
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
{ {
if (part.IsVital && SlotParts.Count(x => x.Value.PartType == part.PartType) == 0) // 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);
damageable.TryChangeDamage(VitalPartsRemovedDamageType, VitalPartsRemovedDamage, true); // TODO BODY KILL EntitySystem.Get<DamageableSystem>().TryChangeDamage(part.Owner.Uid, damage);
}
} }
OnBodyChanged(); OnBodyChanged();
@@ -490,6 +470,7 @@ namespace Content.Shared.Body.Components
} }
} }
private void OnBodyChanged() private void OnBodyChanged()
{ {
Dirty(); 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;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Damage namespace Content.Shared.Damage.Prototypes
{ {
/// <summary> /// <summary>
/// A single damage type. These types are grouped together in <see cref="DamageGroupPrototype"/>s. /// 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 class MedicalScannerBoundUserInterfaceState : BoundUserInterfaceState
{ {
public readonly EntityUid? Entity; public readonly EntityUid? Entity;
public readonly Dictionary<string, int> DamagePerSupportedGroupID; public readonly IReadOnlyDictionary<string, int> DamagePerGroup;
public readonly Dictionary<string, int> DamagePerTypeID; public readonly IReadOnlyDictionary<string, int> DamagePerType;
public readonly bool IsScanned; public readonly bool IsScanned;
public MedicalScannerBoundUserInterfaceState( public MedicalScannerBoundUserInterfaceState(
EntityUid? entity, EntityUid? entity,
Dictionary<string, int> damagePerSupportedGroupID, DamageableComponent? damageable,
Dictionary<string, int> damagePerTypeID,
bool isScanned) bool isScanned)
{ {
Entity = entity; Entity = entity;
DamagePerSupportedGroupID = damagePerSupportedGroupID; DamagePerGroup = damageable?.DamagePerGroup ?? new();
DamagePerTypeID = damagePerTypeID; DamagePerType = damageable?.Damage?.DamageDict ?? new();
IsScanned = isScanned; IsScanned = isScanned;
} }
public bool HasDamage() 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.ActionBlocker;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.MobState.State; using Content.Shared.MobState.State;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
@@ -17,18 +16,20 @@ using Robust.Shared.ViewVariables;
namespace Content.Shared.MobState.Components namespace Content.Shared.MobState.Components
{ {
/// <summary> /// <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. /// this component will handle critical and death behaviors for mobs.
/// Additionally, it handles sending effects to clients /// Additionally, it handles sending effects to clients
/// (such as blur effect for unconsciousness) and managing the health HUD. /// (such as blur effect for unconsciousness) and managing the health HUD.
/// </summary> /// </summary>
[RegisterComponent]
[ComponentReference(typeof(IMobStateComponent))]
[NetworkedComponent()] [NetworkedComponent()]
public abstract class SharedMobStateComponent : Component, IMobStateComponent, IActionBlocker public class MobStateComponent : Component, IMobStateComponent, IActionBlocker
{ {
public override string Name => "MobState"; public override string Name => "MobState";
/// <summary> /// <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. /// the amount of damage at which they are triggered.
/// A threshold is reached when the total damage of an entity is equal /// 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. /// to or higher than the int key, but lower than the next threshold.
@@ -36,7 +37,7 @@ namespace Content.Shared.MobState.Components
/// </summary> /// </summary>
[ViewVariables] [ViewVariables]
[DataField("thresholds")] [DataField("thresholds")]
private readonly SortedDictionary<int, IMobState> _lowestToHighestStates = default!; private readonly SortedDictionary<int, IMobState> _lowestToHighestStates = new();
// TODO Remove Nullability? // TODO Remove Nullability?
[ViewVariables] [ViewVariables]
@@ -53,7 +54,13 @@ namespace Content.Shared.MobState.Components
if (CurrentState != null && CurrentThreshold != null) 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) if (state.CurrentThreshold == null)
{ {
RemoveState(true); RemoveState();
} }
else else
{ {
UpdateState(state.CurrentThreshold.Value, true); UpdateState(state.CurrentThreshold.Value);
}
}
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;
} }
} }
@@ -136,7 +125,7 @@ namespace Content.Shared.MobState.Components
public (IMobState state, int threshold)? GetState(int damage) public (IMobState state, int threshold)? GetState(int damage)
{ {
foreach (var (threshold, state) in _lowestToHighestStates.Reverse()) foreach (var (threshold, state) in _highestToLowestStates)
{ {
if (damage >= threshold) if (damage >= threshold)
{ {
@@ -273,36 +262,32 @@ namespace Content.Shared.MobState.Components
return TryGetState(earliestState, out state, out threshold); return TryGetState(earliestState, out state, out threshold);
} }
private void RemoveState(bool syncing = false) private void RemoveState()
{ {
var old = CurrentState; var old = CurrentState;
CurrentState = null; CurrentState = null;
CurrentThreshold = null; CurrentThreshold = null;
UpdateState(old, null); SetMobState(old, null);
if (!syncing)
{
Dirty();
}
} }
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)) if (!TryGetState(damage, out var newState, out var threshold))
{ {
return; return;
} }
UpdateState(CurrentState, (newState, threshold)); SetMobState(CurrentState, (newState, threshold));
if (!syncing)
{
Dirty();
}
} }
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) if (!current.HasValue)
{ {
@@ -328,9 +313,10 @@ namespace Content.Shared.MobState.Components
state.UpdateState(Owner, threshold); state.UpdateState(Owner, threshold);
var message = new MobStateChangedMessage(this, old, state); var message = new MobStateChangedMessage(this, old, state);
SendMessage(message); SendMessage(message);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message); Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
Dirty();
} }
bool IActionBlocker.CanInteract() 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, [NotNullWhen(true)] out IMobState? state,
out int threshold); 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: behaviors:
- !type:LungBehavior {} - !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 - type: entity
id: OrganHumanHeart id: OrganHumanHeart
parent: BaseHumanOrgan parent: BaseHumanOrgan
@@ -124,36 +126,40 @@
Arithrazine: Arithrazine:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Toxin damage:
healthChange: -1 groups:
- !type:HealthChange Toxin: -2 # -1 Multiplying by 2. pls give damage units
damageGroup: Brute Brute: 1 # 0.5
healthChange: 0.5
Bicaridine: Bicaridine:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Brute damage:
healthChange: -2 groups:
Brute: -2
Dermaline: Dermaline:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Burn damage:
healthChange: -3 groups:
Burn: -3
Dexalin: Dexalin:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Airloss damage:
healthChange: -1 types:
Asphyxiation: -1
DexalinPlus: DexalinPlus:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Airloss damage:
healthChange: -3 types:
Asphyxiation: -3
Dylovene: Dylovene:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Toxin damage:
healthChange: -1 types:
Poison: -1
Ephedrine: Ephedrine:
effects: effects:
- !type:MovespeedModifier - !type:MovespeedModifier
@@ -162,45 +168,45 @@
HeartbreakerToxin: HeartbreakerToxin:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Airloss damage:
healthChange: 4 types:
Asphyxiation: 4
Kelotane: Kelotane:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Burn damage:
healthChange: -1 groups:
Burn: -1
Lexorin: Lexorin:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Airloss damage:
healthChange: 7 groups:
Airloss: 7
Meth: Meth:
effects: effects:
- !type:HealthChange - !type:HealthChange
healthChange: 2.5 damage:
damageGroup: Toxin types:
- !type:MovespeedModifier Poison: 3 # 2.5
walkSpeedModifier: 1.3 - !type:MovespeedModifier
sprintSpeedModifier: 1.3 walkSpeedModifier: 1.3
sprintSpeedModifier: 1.3
Omnizine: Omnizine:
effects: effects:
- !type:HealthChange - !type:HealthChange
healthChange: -2 damage:
damageGroup: Burn groups:
- !type:HealthChange Burn: -3 # -2. w/o damage units did not divide into 3 types
healthChange: -2 Toxin: -2
damageGroup: Toxin Airloss: -2
- !type:HealthChange Brute: -3 # -2. w/o damage units did not divide into 3 types
healthChange: -2
damageGroup: Airloss
- !type:HealthChange
healthChange: -2
damageGroup: Brute
Synaptizine: Synaptizine:
effects: effects:
- !type:HealthChange - !type:HealthChange
damageGroup: Toxin damage:
healthChange: 0.5 types:
Poison: 1 # 0.5 pls damage units
- type: entity - type: entity
id: OrganHumanStomach id: OrganHumanStomach
@@ -261,8 +267,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
- !type:HealthChange - !type:HealthChange
damageGroup: Toxin damage:
healthChange: 1 types:
Poison: 1
JuiceWatermelon: JuiceWatermelon:
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
@@ -306,8 +313,9 @@
- !type:SatiateThirst - !type:SatiateThirst
hydrationFactor: 2 hydrationFactor: 2
- !type:HealthChange - !type:HealthChange
damageGroup: Toxin damage:
healthChange: 1 types:
Poison: 1
- type: entity - type: entity
id: OrganHumanLiver id: OrganHumanLiver

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