Make DamageSpecifier serializable (#43049)

* pt1 damageable refactor

* fixes

* thusd unused

* sparse damagespecifiers

* fix thongs

* aaaaaa

* fix tests

* de-crash
This commit is contained in:
pathetic meowmeow 2026-02-27 01:20:13 -05:00 • committed by GitHub
parent 9cdc009e00
commit e361944ef6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 307 additions and 421 deletions

View file

@ -135,7 +135,7 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
.ToDictionary(x => x.Key, x => x.Value);
IReadOnlyDictionary<string, FixedPoint2> damagePerType = damageable.Damage.DamageDict;
var damagePerType = damageable.Damage.DamageDict;
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
}
@ -153,7 +153,7 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
private void DrawDiagnosticGroups(
Dictionary<string, FixedPoint2> groups,
IReadOnlyDictionary<string, FixedPoint2> damageDict)
IReadOnlyDictionary<ProtoId<DamageTypePrototype>, FixedPoint2> damageDict)
{
GroupsContainer.RemoveAllChildren();

View file

@ -1,6 +1,8 @@
using System.Collections.Generic;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Damageable;
@ -39,14 +41,14 @@ public sealed class DamageSpecifierTest
Assert.That(difference, Is.EqualTo(output5));
}
private static readonly Dictionary<string, FixedPoint2> Input1 = new()
private static readonly Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> Input1 = new()
{
{ "A", 1.5f },
{ "B", 2 },
{ "C", 3 }
};
private static readonly Dictionary<string, FixedPoint2> Input2 = new()
private static readonly Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> Input2 = new()
{
{ "A", 1 },
{ "B", 2 },
@ -54,28 +56,28 @@ public sealed class DamageSpecifierTest
{ "D", 0.05f }
};
private static readonly Dictionary<string, FixedPoint2> Output1 = new()
private static readonly Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> Output1 = new()
{
{ "A", -1.5f },
{ "B", -2 },
{ "C", -3 }
};
private static readonly Dictionary<string, FixedPoint2> Output2 = new()
private static readonly Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> Output2 = new()
{
{ "A", 0.75f },
{ "B", 1 },
{ "C", 1.5 }
};
private static readonly Dictionary<string, FixedPoint2> Output3 = new()
private static readonly Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> Output3 = new()
{
{ "A", 3f },
{ "B", 4 },
{ "C", 6 }
};
private static readonly Dictionary<string, FixedPoint2> Output4 = new()
private static readonly Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> Output4 = new()
{
{ "A", 0.5f },
{ "B", 0 },
@ -83,7 +85,7 @@ public sealed class DamageSpecifierTest
{ "D", -0.05f }
};
private static readonly Dictionary<string, FixedPoint2> Output5 = new()
private static readonly Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> Output5 = new()
{
{ "A", 0.25f },
{ "B", 0 },

View file

@ -148,17 +148,6 @@ namespace Content.IntegrationTests.Tests.Damageable
{
var uid = sDamageableEntity;
// Check that the correct types are supported.
Assert.Multiple(() =>
{
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 = FixedPoint2.New(types.Count * 5);

View file

@ -0,0 +1,20 @@
using System.Text.Json;
using Content.Shared.Damage;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Administration.Logs.Converters;
[AdminLogConverter]
public sealed class DamageSpecifierConverter : AdminLogConverter<DamageSpecifier>
{
public override void Write(Utf8JsonWriter writer, DamageSpecifier value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var (damage, amount) in value.DamageDict)
{
writer.WriteNumber(damage.Id, amount.Double());
}
writer.WriteEndObject();
}
}

View file

@ -67,7 +67,7 @@ public sealed partial class DeltaPressureComponent : Component
[DataField]
public DamageSpecifier BaseDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
DamageDict = new ()
{
{ "Structural", 10 },
},

View file

@ -99,7 +99,7 @@ public sealed partial class ChangelingDevourComponent : Component
[DataField, AutoNetworkedField]
public DamageSpecifier DamagePerTick = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
DamageDict = new ()
{
{ "Slash", 10},
{ "Piercing", 10 },

View file

@ -26,7 +26,7 @@ public sealed partial class CluwneComponent : Component
{
DamageDict = new()
{
{ "Genetic", 300.0 },
{ "Cellular", 300.0 },
},
};

View file

@ -17,7 +17,7 @@ namespace Content.Shared.Damage.Components;
/// may also have resistances to certain damage types, defined via a <see cref="DamageModifierSetPrototype"/>.
/// </remarks>
[RegisterComponent]
[NetworkedComponent]
[NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)]
public sealed partial class DamageableComponent : Component
{
@ -25,7 +25,7 @@ public sealed partial class DamageableComponent : Component
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
/// If null, all damage types will be supported.
/// </summary>
[DataField("damageContainer")]
[DataField("damageContainer"), AutoNetworkedField]
// ReSharper disable once InconsistentNaming - This is wrong but fixing it is potentially annoying for downstreams.
public ProtoId<DamageContainerPrototype>? DamageContainerID;
@ -37,7 +37,7 @@ public sealed partial class DamageableComponent : Component
/// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here
/// to reduce duplication.
/// </remarks>
[DataField("damageModifierSet")]
[DataField("damageModifierSet"), AutoNetworkedField]
public ProtoId<DamageModifierSetPrototype>? DamageModifierSetId;
/// <summary>
@ -46,7 +46,7 @@ public sealed partial class DamageableComponent : Component
/// <remarks>
/// If this data-field is specified, this allows damageable components to be initialized with non-zero damage.
/// </remarks>
[DataField(readOnly: true)] //TODO FULL GAME SAVE
[DataField, AutoNetworkedField]
public DamageSpecifier Damage = new();
/// <summary>
@ -87,20 +87,6 @@ public sealed partial class DamageableComponent : Component
[DataField]
public ProtoId<HealthIconPrototype> RottingIcon = "HealthIconRotting";
[DataField]
[DataField, AutoNetworkedField]
public FixedPoint2? HealthBarThreshold;
}
[Serializable, NetSerializable]
public sealed class DamageableComponentState(
Dictionary<string, FixedPoint2> damageDict,
ProtoId<DamageContainerPrototype>? damageContainerId,
ProtoId<DamageModifierSetPrototype>? modifierSetId,
FixedPoint2? healthBarThreshold)
: ComponentState
{
public readonly Dictionary<string, FixedPoint2> DamageDict = damageDict;
public readonly ProtoId<DamageContainerPrototype>? DamageContainerId = damageContainerId;
public readonly ProtoId<DamageModifierSetPrototype>? ModifierSetId = modifierSetId;
public readonly FixedPoint2? HealthBarThreshold = healthBarThreshold;
}

View file

@ -3,11 +3,10 @@ using System.Text.Json.Serialization;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.Utility;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
namespace Content.Shared.Damage
{
@ -19,31 +18,13 @@ namespace Content.Shared.Damage
/// functions to apply resistance sets and supports basic math operations to modify this dictionary.
/// </remarks>
[DataDefinition, Serializable, NetSerializable]
public sealed partial class DamageSpecifier : IEquatable<DamageSpecifier>
public sealed partial class DamageSpecifier : IEquatable<DamageSpecifier>, IRobustCloneable<DamageSpecifier>
{
// For the record I regret so many of the decisions i made when rewriting damageable
// Why is it just shitting out dictionaries left and right
// One day Arrays, stackalloc spans, and SIMD will save the day.
// TODO DAMAGEABLE REFACTOR
// These exist solely so the wiki works. Please do not touch them or use them.
[JsonPropertyName("types")]
[DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageTypePrototype>))]
[UsedImplicitly]
private Dictionary<string, FixedPoint2>? _damageTypeDictionary;
[JsonPropertyName("groups")]
[DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageGroupPrototype>))]
[UsedImplicitly]
private Dictionary<string, FixedPoint2>? _damageGroupDictionary;
/// <summary>
/// Main DamageSpecifier dictionary. Most DamageSpecifier functions exist to somehow modifying this.
/// </summary>
[JsonIgnore]
[ViewVariables(VVAccess.ReadWrite)]
[IncludeDataField(customTypeSerializer: typeof(DamageSpecifierDictionarySerializer), readOnly: true)]
public Dictionary<string, FixedPoint2> DamageDict { get; set; } = new();
[DataField("types")]
public Dictionary<ProtoId<DamageTypePrototype>, FixedPoint2> DamageDict { get; set; } = new();
/// <summary>
/// Returns a sum of the damage values.
@ -84,6 +65,11 @@ namespace Content.Shared.Damage
[JsonIgnore]
public bool Empty => DamageDict.Count == 0;
public DamageSpecifier Clone()
{
return new DamageSpecifier(this);
}
public override string ToString()
{
return "DamageSpecifier(" + string.Join("; ", DamageDict.Select(x => x.Key + ":" + x.Value)) + ")";

View file

@ -1,81 +0,0 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Content.Shared.Damage;
//todo writing
public sealed class DamageSpecifierDictionarySerializer : ITypeReader<Dictionary<string, FixedPoint2>, MappingDataNode>
{
private ITypeValidator<Dictionary<string, FixedPoint2>, MappingDataNode> _damageTypeSerializer = new PrototypeIdDictionarySerializer<FixedPoint2, DamageTypePrototype>();
private ITypeValidator<Dictionary<string, FixedPoint2>, MappingDataNode> _damageGroupSerializer = new PrototypeIdDictionarySerializer<FixedPoint2, DamageGroupPrototype>();
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
var vals = new Dictionary<ValidationNode, ValidationNode>();
if (node.TryGet<MappingDataNode>("types", out var typesNode))
{
vals.Add(new ValidatedValueNode(new ValueDataNode("types")), _damageTypeSerializer.Validate(serializationManager, typesNode, dependencies, context));
}
if (node.TryGet<MappingDataNode>("groups", out var groupsNode))
{
vals.Add(new ValidatedValueNode(new ValueDataNode("groups")), _damageGroupSerializer.Validate(serializationManager, groupsNode, dependencies, context));
}
return new ValidatedMappingNode(vals);
}
public Dictionary<string, FixedPoint2> Read(ISerializationManager serializationManager, MappingDataNode node, IDependencyCollection dependencies,
SerializationHookContext hookCtx, ISerializationContext? context = null, ISerializationManager.InstantiationDelegate<Dictionary<string, FixedPoint2>>? instanceProvider = null)
{
var dict = instanceProvider != null ? instanceProvider() : new();
// Add all the damage types by just copying the type dictionary (if it is not null).
if (node.TryGet<MappingDataNode>("types", out var typesNode))
{
serializationManager.Read(typesNode, instanceProvider: () => dict, notNullableOverride: true);
}
if (!node.TryGet<MappingDataNode>("groups", out var groupsNode))
return dict;
// Then resolve damage groups and add them
var prototypeManager = dependencies.Resolve<IPrototypeManager>();
foreach (var entry in serializationManager.Read<Dictionary<string, FixedPoint2>>(groupsNode, notNullableOverride: true))
{
if (!prototypeManager.TryIndex<DamageGroupPrototype>(entry.Key, out var group))
{
// This can happen if deserialized before prototypes are loaded.
// i made this a warning bc it was failing tests -paul
dependencies.Resolve<ILogManager>().RootSawmill.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 / FixedPoint2.New(remainingTypes);
if (!dict.TryAdd(damageType, damage))
{
// Key already exists, add values
dict[damageType] += damage;
}
remainingDamage -= damage;
remainingTypes -= 1;
}
}
return dict;
}
}

View file

@ -11,6 +11,7 @@ namespace Content.Shared.Damage.Prototypes
/// to change/get/set damage in a <see cref="DamageableComponent"/>.
/// </remarks>
[Prototype(2)]
[Obsolete("Do not rely on DamageGroupPrototype for anything besides grouping logically similar damage in UIs")]
public sealed partial class DamageGroupPrototype : IPrototype
{
[IdDataField] public string ID { get; private set; } = default!;

View file

@ -8,6 +8,15 @@ namespace Content.Shared.Damage.Systems;
public sealed partial class DamageableSystem
{
/// <returns>If the damage container can take the given damage type</returns>
private bool SupportsType(ProtoId<DamageContainerPrototype>? container, ProtoId<DamageTypePrototype> type)
{
if (container is null)
return true;
return _supportedTypesByContainer[container.Value].Contains(type);
}
/// <summary>
/// Directly sets the damage in a damageable component.
/// This method keeps the damage types supported by the DamageContainerPrototype in the component.
@ -25,30 +34,15 @@ public sealed partial class DamageableSystem
foreach (var type in ent.Comp.Damage.DamageDict.Keys)
{
if (damage.DamageDict.TryGetValue(type, out var value))
ent.Comp.Damage.DamageDict[type] = value;
else
ent.Comp.Damage.DamageDict[type] = 0;
if (!damage.DamageDict.ContainsKey(type))
ent.Comp.Damage.DamageDict.Remove(type);
}
OnEntityDamageChanged((ent, ent.Comp));
}
/// <summary>
/// Directly sets the damage specifier of a damageable component.
/// This will overwrite the complete damage dict, meaning it will bulldoze the supported damage types.
/// </summary>
/// <remarks>
/// This may break persistance as the supported types are reset in case the component is initialized again.
/// So this only makes sense if you also change the DamageContainerPrototype in the component at the same time.
/// Only use this method if you know what you are doing.
/// </remarks>
public void SetDamageSpecifier(Entity<DamageableComponent?> ent, DamageSpecifier damage)
{
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.Damage = damage;
foreach (var (type, amount) in damage.DamageDict)
{
if (SupportsType(ent.Comp.DamageContainerID, type))
ent.Comp.Damage.DamageDict[type] = amount;
}
OnEntityDamageChanged((ent, ent.Comp));
}
@ -167,10 +161,10 @@ public sealed partial class DamageableSystem
var dict = ent.Comp.Damage.DamageDict;
foreach (var (type, value) in damage.DamageDict)
{
// CollectionsMarshal my beloved.
if (!dict.TryGetValue(type, out var oldValue))
if (!SupportsType(ent.Comp.DamageContainerID, type))
continue;
var oldValue = dict.GetValueOrDefault(type);
var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value);
if (newValue == oldValue)
continue;

View file

@ -6,6 +6,7 @@ using Content.Shared.Inventory;
using Content.Shared.Radiation.Events;
using Content.Shared.Rejuvenate;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Damage.Systems;
@ -13,11 +14,13 @@ public sealed partial class DamageableSystem
{
public override void Initialize()
{
RebuildContainerCache();
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
SubscribeLocalEvent<DamageableComponent, ComponentHandleState>(DamageableHandleState);
SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
SubscribeLocalEvent<DamageableComponent, OnIrradiatedEvent>(OnIrradiated);
SubscribeLocalEvent<DamageableComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<DamageableComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_damageableQuery = GetEntityQuery<DamageableComponent>();
@ -119,41 +122,49 @@ public sealed partial class DamageableSystem
);
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
{
if (!ev.WasModified<DamageContainerPrototype>() && !ev.WasModified<DamageGroupPrototype>())
return;
RebuildContainerCache();
}
private void RebuildContainerCache()
{
_supportedTypesByContainer.Clear();
foreach (var proto in _prototypeManager.EnumeratePrototypes<DamageContainerPrototype>())
{
var set = new HashSet<ProtoId<DamageTypePrototype>>();
_supportedTypesByContainer[proto.ID] = set;
foreach (var type in proto.SupportedTypes)
{
set.Add(type);
}
foreach (var groupId in proto.SupportedGroups)
{
var group = _prototypeManager.Index(groupId);
foreach (var type in group.DamageTypes)
{
set.Add(type);
}
}
}
}
private void OnAfterAutoHandleState(Entity<DamageableComponent> ent, ref AfterAutoHandleStateEvent args)
{
OnEntityDamageChanged(ent);
}
/// <summary>
/// Initialize a damageable component
/// </summary>
private void DamageableInit(Entity<DamageableComponent> ent, ref ComponentInit _)
{
if (
ent.Comp.DamageContainerID is null ||
!_prototypeManager.Resolve(ent.Comp.DamageContainerID, out var damageContainerPrototype)
)
{
// No DamageContainerPrototype was given. So we will allow the container to support all damage types
foreach (var type in _prototypeManager.EnumeratePrototypes<DamageTypePrototype>())
{
ent.Comp.Damage.DamageDict.TryAdd(type.ID, FixedPoint2.Zero);
}
}
else
{
// Initialize damage dictionary, using the types and groups from the damage
// container prototype
foreach (var type in damageContainerPrototype.SupportedTypes)
{
ent.Comp.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
}
foreach (var groupId in damageContainerPrototype.SupportedGroups)
{
var group = _prototypeManager.Index(groupId);
foreach (var type in group.DamageTypes)
{
ent.Comp.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
}
}
}
ent.Comp.Damage.GetDamagePerGroup(_prototypeManager, ent.Comp.DamagePerGroup);
ent.Comp.TotalDamage = ent.Comp.Damage.GetTotal();
}
@ -179,28 +190,6 @@ public sealed partial class DamageableSystem
ClearAllDamage(ent.AsNullable());
_mobThreshold.SetAllowRevives(ent, false);
}
private void DamageableHandleState(Entity<DamageableComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not DamageableComponentState state)
return;
ent.Comp.DamageContainerID = state.DamageContainerId;
ent.Comp.DamageModifierSetId = state.ModifierSetId;
ent.Comp.HealthBarThreshold = state.HealthBarThreshold;
// Has the damage actually changed?
DamageSpecifier newDamage = new() { DamageDict = new Dictionary<string, FixedPoint2>(state.DamageDict) };
var delta = newDamage - ent.Comp.Damage;
delta.TrimZeros();
if (delta.Empty)
return;
ent.Comp.Damage = newDamage;
OnEntityDamageChanged(ent, delta);
}
}
/// <summary>

View file

@ -17,7 +17,6 @@ public sealed partial class DamageableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly SharedChemistryGuideDataSystem _chemistryGuideData = default!;
@ -38,6 +37,8 @@ public sealed partial class DamageableSystem : EntitySystem
public float UniversalTopicalsHealModifier { get; private set; } = 1f;
public float UniversalMobDamageModifier { get; private set; } = 1f;
private Dictionary<ProtoId<DamageContainerPrototype>, HashSet<ProtoId<DamageTypePrototype>>> _supportedTypesByContainer = new();
/// <summary>
/// If the damage in a DamageableComponent was changed this function should be called.
/// </summary>
@ -70,31 +71,6 @@ public sealed partial class DamageableSystem : EntitySystem
// byref struct event.
RaiseLocalEvent(ent, new DamageChangedEvent(ent.Comp, damageDelta, interruptsDoAfters, origin));
}
private void DamageableGetState(Entity<DamageableComponent> ent, ref ComponentGetState args)
{
if (_netMan.IsServer)
{
args.State = new DamageableComponentState(
ent.Comp.Damage.DamageDict,
ent.Comp.DamageContainerID,
ent.Comp.DamageModifierSetId,
ent.Comp.HealthBarThreshold
);
// TODO BODY SYSTEM pass damage onto body system
// BOBBY WHEN? 😭
// BOBBY SOON 🫡
return;
}
// avoid mispredicting damage on newly spawned entities.
args.State = new DamageableComponentState(
ent.Comp.Damage.DamageDict.ShallowClone(),
ent.Comp.DamageContainerID,
ent.Comp.DamageModifierSetId,
ent.Comp.HealthBarThreshold
);
}
/// <summary>
/// Goes through an entity damage's and saves them inside a dictionary if the value is higher than 0

View file

@ -28,6 +28,6 @@ public sealed partial class DamageGroupTrigger : IThresholdTrigger
public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
{
return damageable.Comp.DamagePerGroup[DamageGroup] >= Damage;
return damageable.Comp.DamagePerGroup.GetValueOrDefault(DamageGroup) >= Damage;
}
}

View file

@ -13,7 +13,7 @@ public sealed partial class DamageGroupEntityConditionSystem : EntityConditionSy
{
protected override void Condition(Entity<DamageableComponent> entity, ref EntityConditionEvent<DamageGroupCondition> args)
{
var value = entity.Comp.DamagePerGroup[args.Condition.DamageGroup];
var value = entity.Comp.DamagePerGroup.GetValueOrDefault(args.Condition.DamageGroup);
args.Result = value >= args.Condition.Min && value <= args.Condition.Max;
}
}

View file

@ -58,7 +58,7 @@ public sealed partial class KitchenSpikeComponent : Component
[DataField, AutoNetworkedField]
public DamageSpecifier SpikeDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
DamageDict = new()
{
{ "Piercing", 10 },
},
@ -70,7 +70,7 @@ public sealed partial class KitchenSpikeComponent : Component
[DataField, AutoNetworkedField]
public DamageSpecifier ButcherDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
DamageDict = new()
{
{ "Slash", 20 },
},
@ -82,7 +82,7 @@ public sealed partial class KitchenSpikeComponent : Component
[DataField, AutoNetworkedField]
public DamageSpecifier TimeDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
DamageDict = new()
{
{ "Blunt", 1 }, // Mobs are only gibbed from blunt (at least for now).
},

View file

@ -136,12 +136,11 @@
# even if it's a meme - I did research
methods: [ Touch ]
effects: # TODO: when magic is around - make a milk transformation to a skeleton monster
- !type:HealthChange
damage:
groups: # these groups are the only 3 possible ways to damage a skeleton. numerical values obviously up to discussion
Burn: -1
Brute: -0.75
Metaphysical: -2 # Gameplay: skeletons need some way to heal holy damage. Lore: milk is an unholy liquid that shuns the light of god. (/j)
- !type:EvenHealthChange
damage: # these groups are the only 3 possible ways to damage a skeleton. numerical values obviously up to discussion
Burn: -1
Brute: -0.75
Metaphysical: -2 # Gameplay: skeletons need some way to heal holy damage. Lore: milk is an unholy liquid that shuns the light of god. (/j)
- !type:PopupMessage
type: Local
visualType: Large

View file

@ -120,8 +120,9 @@
damage:
types:
Heat: -0.14
groups:
Brute: -0.14
Blunt: -0.05
Piercing: -0.05
Slash: -0.05
- type: DamageVisuals
damageOverlayGroups:
Brute:

View file

@ -229,8 +229,9 @@
types:
Heat: -0.07
Poison: -0.2 # needs to be less than the PendingZombieComponent does or they never become zombies by the disease.
groups:
Brute: -0.07
Blunt: -0.02
Piercing: -0.02
Slash: -0.02
- type: DamageVisuals
damageOverlayGroups:
Brute:

View file

@ -186,8 +186,9 @@
damage:
types:
Heat: -0.07
groups:
Brute: -0.07
Blunt: -0.05
Piercing: -0.05
Slash: -0.05
- type: Fingerprint
- type: Blindable
- type: Temperature

View file

@ -2424,8 +2424,8 @@
angle: 0
animation: WeaponArcBite
damage:
groups:
Brute: 5
types:
Piercing: 5
- type: Item
size: Normal
sprite: Mobs/Animals/grenadepenguin.rsi
@ -2592,9 +2592,12 @@
damage:
types:
Poison: -0.07
groups:
Brute: -0.07
Burn: -0.07
Blunt: -0.03
Slash: -0.03
Piercing: -0.03
Heat: -0.03
Cold: -0.03
Shock: -0.03
- type: entity
parent: MobSpiderBase

View file

@ -102,26 +102,23 @@
- reagents: [ Water ]
methods: [ Touch ]
effects:
- !type:HealthChange
- !type:EvenHealthChange
damage:
groups:
Brute: -0.15
Brute: -0.15
- reagents: [ Blood ]
methods: [ Touch ]
effects:
- !type:HealthChange
- !type:EvenHealthChange
damage:
groups:
Brute: -0.25
Burn: -0.25
Brute: -0.25
Burn: -0.25
- reagents: [ RobustHarvest ]
methods: [ Touch ]
effects:
- !type:HealthChange
- !type:EvenHealthChange
damage:
groups:
Brute: -1
Burn: -1
Brute: -1
Burn: -1
- reagents: [ WeedKiller, PlantBGone ]
methods: [ Touch ]
effects:
@ -202,8 +199,10 @@
- type: MeleeWeapon
hidden: true
damage:
groups:
Brute: 9
types:
Blunt: 3
Piercing: 3
Slash: 3
animation: WeaponArcBite
- type: Climbing
- type: NameIdentifier

View file

@ -77,8 +77,8 @@
angle: 0
animation: WeaponArcBite
damage:
groups:
Brute: 2
types:
Piercing: 2
- type: SolutionContainerManager
solutions:
melee:

View file

@ -78,8 +78,8 @@
collection: AlienClaw
animation: WeaponArcBite
damage:
groups:
Brute: 6
types:
Piercing: 6
- type: DamageStateVisuals
states:
Alive:
@ -176,8 +176,8 @@
40: 0.7
- type: MeleeWeapon
damage:
groups:
Brute: 6
types:
Slash: 6
- type: MovementSpeedModifier
baseSprintSpeed: 4
- type: Fixtures
@ -214,8 +214,8 @@
- type: MovementSpeedModifier
- type: MeleeWeapon
damage:
groups:
Brute: 12
types:
Slash: 12
- type: Fixtures
fixtures:
fix1:
@ -251,8 +251,8 @@
baseSprintSpeed: 4
- type: MeleeWeapon
damage:
groups:
Brute: 10
types:
Slash: 10
- type: SlowOnDamage
speedModifierThresholds:
50: 0.7
@ -284,8 +284,8 @@
baseSprintSpeed: 6.0
- type: MeleeWeapon
damage:
groups:
Brute: 5
types:
Slash: 5
- type: Fixtures
fixtures:
fix1:
@ -506,8 +506,8 @@
collection: AlienClaw
animation: WeaponArcBite
damage:
groups:
Brute: 5
types:
Slash: 5
- type: DamageStateVisuals
states:
Alive:

View file

@ -229,8 +229,8 @@
amount: 1
- type: MeleeWeapon
damage:
groups:
Brute: 12
types:
Piercing: 12
- type: entity
parent: BaseAction

View file

@ -161,8 +161,8 @@
soundHit:
collection: MetalThud
damage:
groups:
Brute: 5
types:
Blunt: 5
# Used for mobs that are affected by atmospherics, pressure, and heat
- type: entity

View file

@ -999,8 +999,8 @@
path: /Audio/Animals/dog_bark1.ogg
hidden: true
damage:
groups:
Brute: 1
types:
Blunt: 1
- type: entity
parent: FoodBreadBase

View file

@ -1121,8 +1121,8 @@
path: /Audio/Animals/cat_hiss.ogg
hidden: true
damage:
groups:
Brute: 1
types:
Blunt: 1
# Suppermatter
# I can't figure out where to put this. It kind of acts like a cake I guess?

View file

@ -68,7 +68,7 @@
Blunt: 1
- type: DamageOtherOnHit
damage:
types:
types: {}
- type: entity
id: BookSpaceEncyclopedia

View file

@ -71,9 +71,10 @@
components:
- type: Repairable # Negative values represent healing
damage:
groups:
Brute: -60 # 20 of each Brute Type - 20% of a basic shield HP
types:
Blunt: -20
Slash: -20
Piercing: -20
Heat: -20
fuelCost: 10 # 1/2 of a Welder for a full repair
doAfterDelay: 5

View file

@ -8,13 +8,19 @@
delay: 10.0
- type: Bible
damage:
groups:
Brute: -15
Burn: -15
types:
Blunt: -5
Piercing: -5
Slash: -5
Heat: -5
Cold: -5
Shock: -5
damageOnFail:
groups:
Brute: 15
Airloss: 15
types:
Blunt: 5
Piercing: 5
Slash: 5
Asphyxiation: 15
damageOnUntrainedUse: ## What a non-chaplain takes when attempting to heal someone
types:
Heat: 10
@ -66,9 +72,11 @@
types:
Caustic: 20 ## ~15 dps
damageOnFail:
groups:
Brute: 15
Airloss: 25
types:
Blunt: 5
Piercing: 5
Slash: 5
Asphyxiation: 25
damageOnUntrainedUse:
types:
Caustic: 50

View file

@ -130,8 +130,10 @@
damageContainers:
- Biological
damage:
groups:
Brute: -15 # 5 for each type in the group
types:
Blunt: -5
Piercing: -5
Slash: -5
healingBeginSound:
path: "/Audio/Items/Medical/brutepack_begin.ogg"
params:
@ -176,8 +178,10 @@
damageContainers:
- Biological
damage:
groups:
Brute: -30 # 10 for each type in the group
types:
Blunt: -10
Piercing: -10
Slash: -10
bloodlossModifier: -10 # a suture should stop ongoing bleeding
healingBeginSound:
path: "/Audio/Items/Medical/brutepack_begin.ogg"
@ -268,9 +272,8 @@
damageContainers:
- Biological
damage:
groups:
Brute: 5 # Tourniquets HURT!
types:
Blunt: 5 # Tourniquets HURT
Asphyxiation: 5 # Essentially Stopping all blood reaching a part of your body
bloodlossModifier: -10 # Tourniquets stop bleeding
delay: 0.5
@ -368,10 +371,14 @@
damageContainers:
- Biological
damage:
groups: # these are all split across multiple types
Brute: -150
Burn: -150
Toxin: -150
types: # these are all split across multiple types
Blunt: -50
Piercing: -50
Slash: -50
Heat: -50
Cold: -50
Shock: -50
Poison: -150
bloodlossModifier: -20
delay: 1
selfHealPenaltyMultiplier: 0

View file

@ -209,8 +209,8 @@
heldPrefix: improv
- type: MeleeWeapon
damage:
groups:
Brute: 10
types:
Slash: 10
soundHit:
path: /Audio/Weapons/bladeslice.ogg
- type: Tool
@ -228,8 +228,8 @@
heldPrefix: electric
- type: MeleeWeapon
damage:
groups:
Brute: 15
types:
Slash: 15
soundHit:
path: /Audio/Items/drill_hit.ogg
- type: Tool

View file

@ -106,8 +106,7 @@
deleteOnCollide: false
onlyCollideWhenShot: true
damage:
types:
Brute: 0
types: {}
- type: Sprite
noRot: true
sprite: Structures/Storage/closet.rsi
@ -246,11 +245,18 @@
color: white
- type: Projectile
damage:
groups: # good enough. Maybe make it call the rejuvenate command later.
Brute: -900
Burn: -900
Airloss: -600
Toxin: -600
types: # good enough. Maybe make it call the rejuvenate command later.
Asphyxiation: -200
Bloodloss: -200
Blunt: -200
Caustic: -200
Cold: -200
Heat: -200
Piercing: -200
Poison: -200
Radiation: -200
Shock: -200
Slash: -200
ignoreResistances: true
- type: entity

View file

@ -18,14 +18,13 @@
params:
volume: -3
damage:
groups:
Brute: 5
types:
Piercing: 5
- type: Wieldable
- type: IncreaseDamageOnWield
damage:
groups:
Brute: 10
types:
Piercing: 10
Structural: 30
- type: Item
size: Normal
@ -62,9 +61,8 @@
path: "/Audio/Items/drill_hit.ogg"
attackRate: 4
damage:
groups:
Brute: 3
types:
Piercing: 3
Structural: 15
- type: entity
@ -84,9 +82,8 @@
path: "/Audio/Items/drill_hit.ogg"
attackRate: 4
damage:
groups:
Brute: 6
types:
Piercing: 6
Structural: 30
@ -132,8 +129,10 @@
delay: 0.9
- type: LeechOnMarker
leech:
groups:
Brute: -6
types:
Blunt: -2
Slash: -2
Piercing: -2
- type: Gun
soundGunshot: /Audio/Weapons/plasma_cutter.ogg
fireRate: 1
@ -238,8 +237,10 @@
count: 1
- type: LeechOnMarker
leech:
groups:
Brute: -21
types:
Blunt: -5
Piercing: -5
Slash: -5
- type: Tag
tags:
- Pickaxe

View file

@ -174,7 +174,7 @@
# why does Flammable even have a required damage
damage:
types:
burn: 0
Heat: 0
considerations:
- !type:TargetDistanceCon
curve: !type:PresetCurve

View file

@ -167,7 +167,7 @@
damage:
types:
Poison: 0.02
- !type:HealthChange
- !type:EvenHealthChange
conditions:
- !type:ReagentCondition
reagent: Ethanol
@ -175,8 +175,7 @@
- !type:MetabolizerTypeCondition
type: [ Dwarf ]
damage:
groups:
Brute: -0.1
Brute: -0.1
- !type:Vomit
probability: 0.04
conditions:
@ -877,13 +876,12 @@
factor: 2
- !type:SatiateHunger
factor: -2
- !type:HealthChange
- !type:EvenHealthChange
damage:
groups:
Burn: -1
Brute: -1
Airloss: -1
Toxin: -1
Burn: -1
Brute: -1
Airloss: -1
Toxin: -1
- type: reagent
id: DriestMartini

View file

@ -29,12 +29,11 @@
metabolisms:
Metabolites: #This makes it not compete with medicines, a large bonus for something that can heal
effects:
- !type:HealthChange
- !type:EvenHealthChange
probability: 0.5
damage:
groups:
Brute: -0.5
Burn: -0.5
Brute: -0.5
Burn: -0.5
# Helps you stop bleeding to an extent.
- !type:ModifyBleed
amount: -0.25
@ -58,11 +57,10 @@
metabolisms:
Metabolites:
effects:
- !type:HealthChange
- !type:EvenHealthChange
probability: 0.5
damage:
groups:
Brute: -0.4
Brute: -0.4
- !type:ModifyBloodLevel
amount: 1 # weaker than iron but pretty good all things considered
- !type:SatiateHunger

View file

@ -25,14 +25,13 @@
- !type:MetabolizerTypeCondition
type: [ Human ]
inverted: true
- !type:HealthChange
- !type:EvenHealthChange
conditions:
- !type:MetabolizerTypeCondition
type: [ Bloodsucker ]
damage:
groups:
Brute: -3
Burn: -1.25
Brute: -3
Burn: -1.25
Bloodstream: # Don't inject someone else's blood into yourself! Your own blood won't actually metabolise.
effects:
- !type:HealthChange

View file

@ -250,7 +250,7 @@
- !type:ReagentCondition
reagent: Ammonia
min: 0.25
- !type:HealthChange
- !type:EvenHealthChange
conditions:
- !type:MetabolizerTypeCondition
type: [ Rat ]
@ -259,11 +259,9 @@
min: 1
ignoreResistances: true
damage:
groups:
Brute: -2.5
Burn: -2.5
types:
Bloodloss: -5
Brute: -2.5
Burn: -2.5
Airloss: -5
- !type:ModifyBloodLevel
amount: 1
conditions:

View file

@ -209,10 +209,9 @@
metabolisms:
Bloodstream:
effects:
- !type:HealthChange
- !type:EvenHealthChange
damage:
groups:
Brute: -4
Brute: -4
- !type:GenericStatusEffect
key: Adrenaline
component: IgnoreSlowOnDamage

View file

@ -39,8 +39,8 @@
reagent: Dylovene
min: 20
damage:
groups:
Brute: 2
types:
Blunt: 2
- !type:Jitter
conditions:
- !type:ReagentCondition
@ -133,8 +133,7 @@
damage:
types:
Radiation: -3
groups:
Brute: 1.5
Blunt: 1.5
- type: reagent
id: Bicaridine
@ -246,8 +245,7 @@
types:
Asphyxiation: 1
Cold: 2
groups:
Brute: 0.5
Blunt: 0.5
- !type:Jitter
conditions:
- !type:ReagentCondition
@ -344,9 +342,17 @@
types:
Asphyxiation: -3
Poison: -0.5
groups:
Brute: -0.5
Burn: -0.5
- !type:EvenHealthChange
conditions:
# they gotta be in crit first
- !type:MobStateCondition
mobstate: Critical
- !type:ReagentCondition
reagent: Epinephrine
max: 20
damage:
Brute: -0.5
Burn: -0.5
- !type:HealthChange
conditions:
- !type:ReagentCondition
@ -626,12 +632,10 @@
metabolisms:
Bloodstream:
effects:
- !type:HealthChange
- !type:EvenHealthChange
damage:
groups:
Brute: -3.5
types:
Asphyxiation: -2.5
Brute: -1.75
Airloss: -1
- !type:HealthChange
conditions:
- !type:ReagentCondition
@ -812,18 +816,14 @@
metabolisms:
Bloodstream:
effects:
- !type:HealthChange
- !type:EvenHealthChange
conditions:
- !type:TotalDamageCondition
max: 50
damage:
groups:
Brute: -1
types:
Poison: -0.5 ##Should be about what it was when it healed the toxin group
Heat: -0.33
Shock: -0.33
Cold: -0.33
Brute: -1
Burn: -1
Toxin: -0.5
- type: reagent
id: Lipozine
@ -887,8 +887,8 @@
min: 0
max: 20
damage:
groups:
Brute: 1.5
types:
Blunt: 1.5
- !type:EvenHealthChange
conditions:
- !type:ReagentCondition
@ -902,8 +902,8 @@
reagent: Ultravasculine
min: 20
damage:
groups:
Brute: 6
types:
Blunt: 6
- !type:AdjustReagent
conditions:
- !type:ReagentCondition

View file

@ -17,7 +17,7 @@
damage:
types:
Poison: 0.55
Blunt: 0.5
Blunt: 1.1 # meant to be slightly more than tricordrazine heals
# OD
- !type:HealthChange
conditions:

View file

@ -332,8 +332,9 @@
effects:
- !type:HealthChange
damage:
groups:
Airloss: 10
types:
Asphyxiation: 5
Bloodloss: 5
- type: reagent
id: MindbreakerToxin
@ -368,8 +369,8 @@
- !type:HealthChange
probability: 0.1
damage:
groups:
Brute: 2
types:
Blunt: 2
# todo: cough, sneeze
- !type:HealthChange
conditions:
@ -377,10 +378,9 @@
reagent: Histamine
min: 45
damage:
groups:
Brute: 2
Airloss: 2
types:
Blunt: 2
Asphyxiation: 2
Poison: 2
- !type:PopupMessage
type: Local
@ -660,8 +660,9 @@
sprintSpeedModifier: 0.8
- !type:HealthChange
damage:
groups:
Airloss: 2
types:
Asphyxiation: 1
Bloodloss: 1
- type: reagent
id: Tazinide

View file

@ -1256,9 +1256,13 @@
components:
- MobState
damage:
groups:
Brute: -300
Burn: -300
types:
Blunt: -100
Piercing: -100
Slash: -100
Heat: -100
Cold: -100
Shock: -100
- type: entity
id: XenoArtifactTesla