Files
tbd-station-14/Content.Shared/Damage/Systems/DamageableSystem.cs
Fildrance d38042a0c0 3mo xeno archeology (first phase) (#33370)
* DAG Adjacency Matrix & Tests

* Fix sandbox type errors

* First pass on procgen

* Procgen adjustments

* Networking

* Cruft and god and beauty and analysis console

* convert to data types that dont make me want to kill myself

* starting work on console UI

* drawing nodes n shit

* damn that ui FUCKS

* XAT

* Add a bunch of basic triggers

* Fix trigger gen

* Add node info into the analysis console UI

* Add node unlocking

* more trigger cuz thats pretty cool

* final triggers + incorporate gnostic faith

* some ui changes, mostly

* Fix orphaned procgen segments

* its not random dipshit

* yeah... this one will make pjb happy....

* we call it a day for the UI

* imagine... shared power code...

* extraction WIP but we gotta sidequest momentarily

* oh hey would you look at that its the actual functionality

* distrotrased

* Small departure for randomness.

* ok yep yep indeed that is an effect very cool.

* thanos snap oldcode

* fuck it we ball

* feat: node scanner now displays triggered nodes. Removed unused old artifact systems and related code (most of it). xml-doc and minor fixups.

* refactor: most of preparations, cleanup and groundwork. also segment-related tests

* feature: all basic effects returning

* feat: finished effects lits, created weight lists for struct and handheld artifacts, fixed throw trigger and music ApplyComponent artifact effects not working

* feat: prevent non-first-time-predicted calls in shared artifact effect systems

* fix: remove gun effect from artifact effects - as it interferes with 'activate artefact' action

* fix: foam reagent selection, neat ApplyComponents art effect scenarios, handheld art is RadiationReceiver again

* fix: moved spawn/ pry&throw effect systems back to server part of code - entity duplication bugs were not quite fun

* refactor: fix protos

* refactor: fix linter

* fix: fix old artifact component names in yml

* fix: no more throwing error on artifact spawn with empty XAEFoamComponent.Reagents

* fix: removed old component usage in maps

* fix: remove more deleted components from map

* fix: ContainerContainer is now part of initial artifact entity, it won't be affecting UninitializedSaveTest

* refactor: fix tests, add loc description to toolshed commands

* Changed node scanner to tell the whole story about current artifact state

* refactor: remove excessive get of EntityCoordinates in XAE systems, removed Value access in NodeScannerDisplay

* fix: turned off TriggerInteraction, removed XAESpawn usage and system, EmpSystem now can use EntityCoordinates,

* fix: moved SharedXenoArtifactSystem.CancelUnlockingOnGraphStructureChange into RebuildXenoArtifactMetaData to lessen code coupling

* fix: XenoArtifactEffectJunkSpawn moved invalid rolls declaration

* refactor: set default value for XenoArtifactComponent.EffectsTable for tests

* fix: now explosions XAE can be activated for effect

* refactor: added some usedelay so artifactuse would'nt be spammed

* refactor: artifact-related hints improvements

* fix: artifact no longer spawns fauna into itself

* refactor: xml-doc and minor refactoring

* refactor: xml-doc for Xeno Artifact systems, renaming of questionable XAT systems

* map for playtest, TODO REVERT THIS

* fix: magboots trigger art from a mile

* refactor: bind artifact animation to unlocking state

* feat: radiation dmg now have reference to source (and artifacts won't irradiate themselves)

* fix: random artifact node durability now is rolled for max and not current value

* refactor: gas effects are more rare, hand-held artifact effects are filtered properly now, rad dmg trigger now requires only 20 dmg for activation

* feat: animations and sound effects for artifact force-use and failed finish of unlocking phase

* use only 1 file with art use animation

* refactor: minor artifact dmg triggers tuning

* feat: now nodes that CAN be unlocked are displayed with specific color in console.

* feat: now unlocking stage time is dynamic and it depends on amount of triggers player activated correctly. Failed one stops incrementing

* feat: now non-active unlocked nodes return more points if durability was not wasted

* feat: now puddle/foam effects change description of node

* fix: fix test failure

* refactor: renamed phasing effect, fixed failing test for elkridge

* minor balance changes

* refactor: split rare materials into separate effects

* feat: unlocked nodes without successor wont listen to unlocks, node unlock is not activating node

* fix: removed OnIrradiatedEvent duplicate c-tor

* revert changes of reach for playtest

* revert last row empty line removal on reach.yml

* fix: fix PVS bug, born from attempt to relay event to art nodes that were not synced yet to the client

* fix: fix elkridge for tests (again)

* refactor: xml-doc, more stuff predicted, allocation optimization in XAE/XAT systems

* refactor: naming

* refactor: extract variable refactor for XAEApplyComponentsSystem.OnActivated insides

* fix: duplicate xeno artifact unlocking sound fixed

* feat: CreatePuddle xeno artifact effect now can have min and max borders for chamicals to be drafted, minor XAECreatePuddleSystem refactor

* feat: networking for shared XAE components + xml-doc leftovers

* refactor: more xml-doc, fix XAEApplyComponentsComponent.Components not being serializable but trying to be

* refactor: xml-docs and XAEThrowThingsAroundSystem now uses circle and not box for prying tiles

* refactor: xml-docs, minor refactors

* revert XenoArtifactCommand.ArtifactPrototype being PrototId

* refactor: simplify the way ExtractionResearchLabel works

---------

Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2025-04-15 10:34:53 +10:00

453 lines
19 KiB
C#

using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Chemistry;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Radiation.Events;
using Content.Shared.Rejuvenate;
using Robust.Shared.Configuration;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Damage
{
public sealed 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!;
private EntityQuery<AppearanceComponent> _appearanceQuery;
private EntityQuery<DamageableComponent> _damageableQuery;
private EntityQuery<MindContainerComponent> _mindContainerQuery;
public float UniversalAllDamageModifier { get; private set; } = 1f;
public float UniversalAllHealModifier { get; private set; } = 1f;
public float UniversalMeleeDamageModifier { get; private set; } = 1f;
public float UniversalProjectileDamageModifier { get; private set; } = 1f;
public float UniversalHitscanDamageModifier { get; private set; } = 1f;
public float UniversalReagentDamageModifier { get; private set; } = 1f;
public float UniversalReagentHealModifier { get; private set; } = 1f;
public float UniversalExplosionDamageModifier { get; private set; } = 1f;
public float UniversalThrownDamageModifier { get; private set; } = 1f;
public float UniversalTopicalsHealModifier { get; private set; } = 1f;
public float UniversalMobDamageModifier { get; private set; } = 1f;
public override void Initialize()
{
SubscribeLocalEvent<DamageableComponent, ComponentInit>(DamageableInit);
SubscribeLocalEvent<DamageableComponent, ComponentHandleState>(DamageableHandleState);
SubscribeLocalEvent<DamageableComponent, ComponentGetState>(DamageableGetState);
SubscribeLocalEvent<DamageableComponent, OnIrradiatedEvent>(OnIrradiated);
SubscribeLocalEvent<DamageableComponent, RejuvenateEvent>(OnRejuvenate);
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_damageableQuery = GetEntityQuery<DamageableComponent>();
_mindContainerQuery = GetEntityQuery<MindContainerComponent>();
// Damage modifier CVars are updated and stored here to be queried in other systems.
// Note that certain modifiers requires reloading the guidebook.
Subs.CVar(_config, CCVars.PlaytestAllDamageModifier, value =>
{
UniversalAllDamageModifier = value;
_chemistryGuideData.ReloadAllReagentPrototypes();
}, true);
Subs.CVar(_config, CCVars.PlaytestAllHealModifier, value =>
{
UniversalAllHealModifier = value;
_chemistryGuideData.ReloadAllReagentPrototypes();
}, true);
Subs.CVar(_config, CCVars.PlaytestProjectileDamageModifier, value => UniversalProjectileDamageModifier = value, true);
Subs.CVar(_config, CCVars.PlaytestMeleeDamageModifier, value => UniversalMeleeDamageModifier = value, true);
Subs.CVar(_config, CCVars.PlaytestProjectileDamageModifier, value => UniversalProjectileDamageModifier = value, true);
Subs.CVar(_config, CCVars.PlaytestHitscanDamageModifier, value => UniversalHitscanDamageModifier = value, true);
Subs.CVar(_config, CCVars.PlaytestReagentDamageModifier, value =>
{
UniversalReagentDamageModifier = value;
_chemistryGuideData.ReloadAllReagentPrototypes();
}, true);
Subs.CVar(_config, CCVars.PlaytestReagentHealModifier, value =>
{
UniversalReagentHealModifier = value;
_chemistryGuideData.ReloadAllReagentPrototypes();
}, true);
Subs.CVar(_config, CCVars.PlaytestExplosionDamageModifier, value => UniversalExplosionDamageModifier = value, true);
Subs.CVar(_config, CCVars.PlaytestThrownDamageModifier, value => UniversalThrownDamageModifier = value, true);
Subs.CVar(_config, CCVars.PlaytestTopicalsHealModifier, value => UniversalTopicalsHealModifier = value, true);
Subs.CVar(_config, CCVars.PlaytestMobDamageModifier, value => UniversalMobDamageModifier = value, true);
}
/// <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, FixedPoint2.Zero);
}
foreach (var groupId in damageContainerPrototype.SupportedGroups)
{
var group = _prototypeManager.Index<DamageGroupPrototype>(groupId);
foreach (var type in group.DamageTypes)
{
component.Damage.DamageDict.TryAdd(type, FixedPoint2.Zero);
}
}
}
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, FixedPoint2.Zero);
}
}
component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
component.TotalDamage = component.Damage.GetTotal();
}
/// <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(EntityUid uid, DamageableComponent damageable, DamageSpecifier damage)
{
damageable.Damage = damage;
DamageChanged(uid, 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(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null,
bool interruptsDoAfters = true, EntityUid? origin = null)
{
component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
component.TotalDamage = component.Damage.GetTotal();
Dirty(uid, component);
if (_appearanceQuery.TryGetComponent(uid, out var appearance) && damageDelta != null)
{
var data = new DamageVisualizerGroupData(component.DamagePerGroup.Keys.ToList());
_appearance.SetData(uid, DamageVisualizerKeys.DamageUpdateGroups, data, appearance);
}
RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin));
}
/// <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,
bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null)
{
if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false))
{
// TODO BODY SYSTEM pass damage onto body system
return null;
}
if (damage.Empty)
{
return damage;
}
var before = new BeforeDamageChangedEvent(damage, origin);
RaiseLocalEvent(uid.Value, ref before);
if (before.Cancelled)
return null;
// Apply resistances
if (!ignoreResistances)
{
if (damageable.DamageModifierSetId != null &&
_prototypeManager.TryIndex<DamageModifierSetPrototype>(damageable.DamageModifierSetId, out var modifierSet))
{
// TODO DAMAGE PERFORMANCE
// use a local private field instead of creating a new dictionary here..
damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
}
var ev = new DamageModifyEvent(damage, origin);
RaiseLocalEvent(uid.Value, ev);
damage = ev.Damage;
if (damage.Empty)
{
return damage;
}
}
damage = ApplyUniversalAllModifiers(damage);
// TODO DAMAGE PERFORMANCE
// Consider using a local private field instead of creating a new dictionary here.
// Would need to check that nothing ever tries to cache the delta.
var delta = new DamageSpecifier();
delta.DamageDict.EnsureCapacity(damage.DamageDict.Count);
var dict = damageable.Damage.DamageDict;
foreach (var (type, value) in damage.DamageDict)
{
// CollectionsMarshal my beloved.
if (!dict.TryGetValue(type, out var oldValue))
continue;
var newValue = FixedPoint2.Max(FixedPoint2.Zero, oldValue + value);
if (newValue == oldValue)
continue;
dict[type] = newValue;
delta.DamageDict[type] = newValue - oldValue;
}
if (delta.DamageDict.Count > 0)
DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin);
return delta;
}
/// <summary>
/// Applies the two univeral "All" modifiers, if set.
/// Individual damage source modifiers are set in their respective code.
/// </summary>
/// <param name="damage">The damage to be changed.</param>
public DamageSpecifier ApplyUniversalAllModifiers(DamageSpecifier damage)
{
// Checks for changes first since they're unlikely in normal play.
if (UniversalAllDamageModifier == 1f && UniversalAllHealModifier == 1f)
return damage;
foreach (var (key, value) in damage.DamageDict)
{
if (value == 0)
continue;
if (value > 0)
{
damage.DamageDict[key] *= UniversalAllDamageModifier;
continue;
}
if (value < 0)
{
damage.DamageDict[key] *= UniversalAllHealModifier;
}
}
return damage;
}
/// <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(EntityUid uid, DamageableComponent component, FixedPoint2 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(uid, component, new DamageSpecifier());
}
public void SetDamageModifierSetId(EntityUid uid, string? damageModifierSetId, DamageableComponent? comp = null)
{
if (!_damageableQuery.Resolve(uid, ref comp))
return;
comp.DamageModifierSetId = damageModifierSetId;
Dirty(uid, comp);
}
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
{
if (_netMan.IsServer)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold);
}
else
{
// avoid mispredicting damage on newly spawned entities.
args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold);
}
}
private void OnIrradiated(EntityUid uid, DamageableComponent component, OnIrradiatedEvent args)
{
var damageValue = FixedPoint2.New(args.TotalRads);
// Radiation should really just be a damage group instead of a list of types.
DamageSpecifier damage = new();
foreach (var typeId in component.RadiationDamageTypeIDs)
{
damage.DamageDict.Add(typeId, damageValue);
}
TryChangeDamage(uid, damage, interruptsDoAfters: false, origin: args.Origin);
}
private void OnRejuvenate(EntityUid uid, DamageableComponent component, RejuvenateEvent args)
{
TryComp<MobThresholdsComponent>(uid, out var thresholds);
_mobThreshold.SetAllowRevives(uid, true, thresholds); // do this so that the state changes when we set the damage
SetAllDamage(uid, component, 0);
_mobThreshold.SetAllowRevives(uid, false, thresholds);
}
private void DamageableHandleState(EntityUid uid, DamageableComponent component, ref ComponentHandleState args)
{
if (args.Current is not DamageableComponentState state)
{
return;
}
component.DamageContainerID = state.DamageContainerId;
component.DamageModifierSetId = state.ModifierSetId;
component.HealthBarThreshold = state.HealthBarThreshold;
// Has the damage actually changed?
DamageSpecifier newDamage = new() { DamageDict = new(state.DamageDict) };
var delta = component.Damage - newDamage;
delta.TrimZeros();
if (!delta.Empty)
{
component.Damage = newDamage;
DamageChanged(uid, component, delta);
}
}
}
/// <summary>
/// Raised before damage is done, so stuff can cancel it if necessary.
/// </summary>
[ByRefEvent]
public record struct BeforeDamageChangedEvent(DamageSpecifier Damage, EntityUid? Origin = null, bool Cancelled = false);
/// <summary>
/// Raised on an entity when damage is about to be dealt,
/// in case anything else needs to modify it other than the base
/// damageable component.
///
/// For example, armor.
/// </summary>
public sealed class DamageModifyEvent : EntityEventArgs, IInventoryRelayEvent
{
// Whenever locational damage is a thing, this should just check only that bit of armour.
public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET;
public readonly DamageSpecifier OriginalDamage;
public DamageSpecifier Damage;
public EntityUid? Origin;
public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null)
{
OriginalDamage = damage;
Damage = damage;
Origin = origin;
}
}
public sealed 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;
/// <summary>
/// Does this event interrupt DoAfters?
/// Note: As provided in the constructor, this *does not* account for DamageIncreased.
/// As written into the event, this *does* account for DamageIncreased.
/// </summary>
public readonly bool InterruptsDoAfters;
/// <summary>
/// Contains the entity which caused the change in damage, if any was responsible.
/// </summary>
public readonly EntityUid? Origin;
public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin)
{
Damageable = damageable;
DamageDelta = damageDelta;
Origin = origin;
if (DamageDelta == null)
return;
foreach (var damageChange in DamageDelta.DamageDict.Values)
{
if (damageChange > 0)
{
DamageIncreased = true;
break;
}
}
InterruptsDoAfters = interruptsDoAfters && DamageIncreased;
}
}
}