Files
tbd-station-14/Content.Tests/Shared/DamageTest.cs
Pieter-Jan Briers 33611b7094 Solution precision fixes (#25199)
* Add test for two chemistry issues

1. rounding issue with reaction processing when making chloral hydrate
2. reliable assert trip due to the ValidateSolution() heat capacity issue.

* Fix FixedPoint2 arithmetic

Fix internal floating point arithmetic in places where it could be avoided.

Fix incorrect rounding mode used in other places (it should always floor, like regular int arithmetic).

I had to add an explicit epsilon value for float -> FixedPoint2 because something like 1.05 is actually like 1.04999 and that'd cause it to be rounded down to 1.04.

This fixes reaction reagent processing in cases where the reagent inputs can't cleanly divide. Previously, when making 30u chloral hydrate by adding the chlorine in 10u increments you'd end up with 0.04 chlorine left over. This was caused by division in the reaction code rounding up in some cases. Changing division here to always round down fixes it.

* Attempt to fix heat capacity precision assert issues.

Fixes #22126

First, we just increase the tolerance of the assert. It was way too low.

Second, actually put a cap on float drift from one-off _heatCapacity changes.

* Fix float -> FixedPoint2 epsilon for negative number, fix tests.

* Fix DamageableTest

* Oh yeah I need to call CleanReturnAsync
2024-02-16 16:54:27 -07:00

295 lines
11 KiB
C#

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;
using Content.Shared.FixedPoint;
namespace Content.Tests.Shared
{
// Basic tests of various damage prototypes and classes.
[TestFixture]
[TestOf(typeof(DamageSpecifier))]
[TestOf(typeof(DamageModifierSetPrototype))]
[TestOf(typeof(DamageGroupPrototype))]
public sealed class DamageTest : ContentUnitTest
{
private static Dictionary<string, float> _resistanceCoefficientDict = new()
{
// "missing" blunt entry
{ "Piercing", -2 },// Turn Piercing into Healing
{ "Slash", 3 },
{ "Radiation", 1.5f },
};
private static 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.ResolveResults();
// 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
FixedPoint2 damage;
Assert.That(damageSpec.GetTotal(), Is.EqualTo(FixedPoint2.New(8)));
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(2)));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(2)));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(1)));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(3)));
// check that integer multiplication works
damageSpec = damageSpec * 2;
Assert.That(damageSpec.GetTotal(), Is.EqualTo(FixedPoint2.New(16)));
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(4)));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(4)));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(2)));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(6)));
// check that float multiplication works
damageSpec = damageSpec * 2.2f;
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(8.8)));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(8.8)));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(4.4)));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(13.2)));
Assert.That(damageSpec.GetTotal(), Is.EqualTo(FixedPoint2.New(8.8 + 8.8 + 4.4 + 13.2)));
// check that integer division works
damageSpec = damageSpec / 2;
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(4.4)));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(4.4)));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(2.2)));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(6.6)));
// check that float division works
damageSpec = damageSpec / 2.2f;
Assert.That(damageSpec.DamageDict.TryGetValue("Blunt", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(2)));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(2)));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(1)));
Assert.That(damageSpec.DamageDict.TryGetValue("Radiation", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(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(FixedPoint2.New(1.33)));
Assert.That(damageSpec.DamageDict.TryGetValue("Slash", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(1.33)));
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(1.34))); // doesn't divide evenly, so the 0.01 goes to the last one
damageSpec = new(_prototypeManager.Index<DamageTypePrototype>("Piercing"), 4);
Assert.That(damageSpec.DamageDict.TryGetValue("Piercing", out damage));
Assert.That(damage, Is.EqualTo(FixedPoint2.New(4)));
}
//Check that DamageSpecifier will be properly adjusted by a resistance set
[Test]
public void ModifierSetTest()
{
// Create a copy of the damage data
DamageSpecifier damageSpec = 10 * new DamageSpecifier(_damageSpec);
// Create a modifier set
DamageModifierSetPrototype modifierSet = 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.5
// Apply once
damageSpec = DamageSpecifier.ApplyModifierSet(damageSpec, modifierSet);
Assert.That(damageSpec.DamageDict["Blunt"], Is.EqualTo(FixedPoint2.New(25)));
Assert.That(damageSpec.DamageDict["Piercing"], Is.EqualTo(FixedPoint2.New(-40))); // became healing
Assert.That(damageSpec.DamageDict["Slash"], Is.EqualTo(FixedPoint2.New(6)));
Assert.That(damageSpec.DamageDict["Radiation"], Is.EqualTo(FixedPoint2.New(44.25)));
// And again, checking for some other behavior
damageSpec = DamageSpecifier.ApplyModifierSet(damageSpec, modifierSet);
Assert.That(damageSpec.DamageDict["Blunt"], Is.EqualTo(FixedPoint2.New(30)));
Assert.That(damageSpec.DamageDict["Piercing"], Is.EqualTo(FixedPoint2.New(-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(FixedPoint2.New(65.62)));
}
// 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: damageModifierSet
id: Metallic
coefficients:
Blunt: 0.7
Slash: 0.5
Piercing: 0.7
Shock: 1.2
flatReductions:
Blunt: 5
- type: damageModifierSet
id: Inflatable
coefficients:
Blunt: 0.5
Piercing: 2.0
Heat: 0.5
Shock: 0
flatReductions:
Blunt: 5
- type: damageModifierSet
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
";
}
}