Merge branch 'master' into offmed-staging
This commit is contained in:
@@ -10,6 +10,7 @@ using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.StationRecords;
|
||||
@@ -44,6 +45,8 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
SubscribeLocalEvent<AccessReaderComponent, GotEmaggedEvent>(OnEmagged);
|
||||
SubscribeLocalEvent<AccessReaderComponent, LinkAttemptEvent>(OnLinkAttempt);
|
||||
SubscribeLocalEvent<AccessReaderComponent, AccessReaderConfigurationAttemptEvent>(OnConfigurationAttempt);
|
||||
SubscribeLocalEvent<AccessReaderComponent, FindAvailableLocksEvent>(OnFindAvailableLocks);
|
||||
SubscribeLocalEvent<AccessReaderComponent, CheckUserHasLockAccessEvent>(OnCheckLockAccess);
|
||||
|
||||
SubscribeLocalEvent<AccessReaderComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<AccessReaderComponent, ComponentHandleState>(OnHandleState);
|
||||
@@ -169,6 +172,22 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
ent.Comp.AccessListsOriginal ??= new(ent.Comp.AccessLists);
|
||||
}
|
||||
|
||||
private void OnFindAvailableLocks(Entity<AccessReaderComponent> ent, ref FindAvailableLocksEvent args)
|
||||
{
|
||||
args.FoundReaders |= LockTypes.Access;
|
||||
}
|
||||
|
||||
private void OnCheckLockAccess(Entity<AccessReaderComponent> ent, ref CheckUserHasLockAccessEvent args)
|
||||
{
|
||||
// Are we looking for an access lock?
|
||||
if (!args.FoundReaders.HasFlag(LockTypes.Access))
|
||||
return;
|
||||
|
||||
// If the user has access to this lock, we pass it into the event.
|
||||
if (IsAllowed(args.User, ent))
|
||||
args.HasAccess |= LockTypes.Access;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the source for access tags
|
||||
/// then compares it with the all targets accesses to see if it is allowed.
|
||||
|
||||
@@ -167,15 +167,21 @@ namespace Content.Shared.ActionBlocker
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
public bool CanPickup(EntityUid user, EntityUid item)
|
||||
/// <summary>
|
||||
/// Whether a user can pickup the given item.
|
||||
/// </summary>
|
||||
/// <param name="user">The mob trying to pick up the item.</param>
|
||||
/// <param name="item">The item being picked up.</param>
|
||||
/// <param name="showPopup">Whether or not to show a popup to the player telling them why the attempt failed.</param>
|
||||
public bool CanPickup(EntityUid user, EntityUid item, bool showPopup = false)
|
||||
{
|
||||
var userEv = new PickupAttemptEvent(user, item);
|
||||
var userEv = new PickupAttemptEvent(user, item, showPopup);
|
||||
RaiseLocalEvent(user, userEv);
|
||||
|
||||
if (userEv.Cancelled)
|
||||
return false;
|
||||
|
||||
var itemEv = new GettingPickedUpAttemptEvent(user, item);
|
||||
var itemEv = new GettingPickedUpAttemptEvent(user, item, showPopup);
|
||||
RaiseLocalEvent(item, itemEv);
|
||||
|
||||
return !itemEv.Cancelled;
|
||||
|
||||
14
Content.Shared/Administration/Systems/RejuvenateSystem.cs
Normal file
14
Content.Shared/Administration/Systems/RejuvenateSystem.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.Rejuvenate;
|
||||
|
||||
namespace Content.Shared.Administration.Systems;
|
||||
|
||||
public sealed class RejuvenateSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Fully heals the target, removing all damage, debuffs or other negative status effects.
|
||||
/// </summary>
|
||||
public void PerformRejuvenate(EntityUid target)
|
||||
{
|
||||
RaiseLocalEvent(target, new RejuvenateEvent());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Silicons.Borgs;
|
||||
|
||||
139
Content.Shared/Atmos/Components/DeltaPressureComponent.cs
Normal file
139
Content.Shared/Atmos/Components/DeltaPressureComponent.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Atmos.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Entities that have this component will have damage done to them depending on the local pressure
|
||||
/// environment that they reside in.
|
||||
///
|
||||
/// Atmospherics.DeltaPressure batch-processes entities with this component in a list on
|
||||
/// the grid's GridAtmosphereComponent.
|
||||
/// The entities are automatically added and removed from this list, and automatically
|
||||
/// added on initialization.
|
||||
/// </summary>
|
||||
/// <remarks> Note that the entity should have an AirtightComponent and be a grid structure.</remarks>
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent, AutoGenerateComponentState]
|
||||
[Access(typeof(SharedAtmosphereSystem), typeof(SharedDeltaPressureSystem))]
|
||||
public sealed partial class DeltaPressureComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the entity is currently in the processing list of the grid's GridAtmosphereComponent.
|
||||
/// </summary>
|
||||
[DataField(readOnly: true)]
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public bool InProcessingList;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity is currently taking damage from pressure.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool IsTakingDamage;
|
||||
|
||||
/// <summary>
|
||||
/// The grid this entity is currently joined to for processing.
|
||||
/// Required for proper deletion, as we cannot reference the grid
|
||||
/// for removal while the entity is being deleted.
|
||||
/// </summary>
|
||||
/// <remarks>Note that while AirtightComponent already stores the grid,
|
||||
/// we cannot trust it to be available on init or when the entity is being deleted. Tragic.
|
||||
/// Double note: this is set during ComponentInit and thus does not need to be a datafield
|
||||
/// or else it will spam serialization.</remarks>
|
||||
/// TODO ATMOS: Simply use AirtightComponent's GridUID caching and handle entity removal from the processing list on an invalidation system similar to InvalidTiles.
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public EntityUid? GridUid;
|
||||
|
||||
/// <summary>
|
||||
/// The percent chance that the entity will take damage each atmos tick,
|
||||
/// when the entity is above the damage threshold.
|
||||
/// Makes it so that windows don't all break in one go.
|
||||
/// Float is from 0 to 1, where 1 means 100% chance.
|
||||
/// If this is set to 0, the entity will never take damage.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float RandomDamageChance = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// The base damage applied to the entity per atmos tick when it is above the damage threshold.
|
||||
/// This damage will be scaled as defined by the <see cref="DeltaPressureDamageScalingType"/> enum
|
||||
/// depending on the current effective pressure this entity is experiencing.
|
||||
/// Note that this damage will scale depending on the pressure above the minimum pressure,
|
||||
/// not at the current pressure.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public DamageSpecifier BaseDamage = new()
|
||||
{
|
||||
DamageDict = new Dictionary<string, FixedPoint2>
|
||||
{
|
||||
{ "Structural", 10 },
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The minimum pressure in kPa at which the entity will start taking damage.
|
||||
/// This doesn't depend on the difference in pressure.
|
||||
/// The entity will start to take damage if it is exposed to this pressure.
|
||||
/// This is needed because we don't correctly handle 2-layer windows yet.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MinPressure = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum difference in pressure between any side required for the entity to start taking damage.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[GuidebookData]
|
||||
public float MinPressureDelta = 7500;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum pressure at which damage will no longer scale.
|
||||
/// If the effective pressure goes beyond this, the damage will be considered at this pressure.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxEffectivePressure = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Simple constant to affect the scaling behavior.
|
||||
/// See comments in the <see cref="DeltaPressureDamageScalingType"/> types to see how this affects scaling.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ScalingPower = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the scaling behavior for the damage.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public DeltaPressureDamageScalingType ScalingType = DeltaPressureDamageScalingType.Threshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An enum that defines how the damage dealt by the <see cref="DeltaPressureComponent"/> scales
|
||||
/// depending on the pressure experienced by the entity.
|
||||
/// The scaling is done on the effective pressure, which is the pressure above the minimum pressure.
|
||||
/// See https://www.desmos.com/calculator/9ctlq3zpnt for a visual representation of the scaling types.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public enum DeltaPressureDamageScalingType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Damage dealt will be constant as long as the minimum values are met.
|
||||
/// Scaling power is ignored.
|
||||
/// </summary>
|
||||
Threshold,
|
||||
|
||||
/// <summary>
|
||||
/// Damage dealt will be a linear function.
|
||||
/// Scaling power determines the slope of the function.
|
||||
/// </summary>
|
||||
Linear,
|
||||
|
||||
/// <summary>
|
||||
/// Damage dealt will be a logarithmic function.
|
||||
/// Scaling power determines the base of the log.
|
||||
/// </summary>
|
||||
Log,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Examine;
|
||||
|
||||
namespace Content.Shared.Atmos.EntitySystems;
|
||||
|
||||
public abstract partial class SharedDeltaPressureSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DeltaPressureComponent, ExaminedEvent>(OnExaminedEvent);
|
||||
}
|
||||
|
||||
private void OnExaminedEvent(Entity<DeltaPressureComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (ent.Comp.IsTakingDamage)
|
||||
args.PushMarkup(Loc.GetString("window-taking-damage"));
|
||||
}
|
||||
}
|
||||
10
Content.Shared/Atmos/TileFireEvent.cs
Normal file
10
Content.Shared/Atmos/TileFireEvent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Content.Shared.Atmos;
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on an entity when it is standing on a tile that's on fire.
|
||||
/// </summary>
|
||||
/// <param name="Temperature">Current temperature of the hotspot this entity is exposed to.</param>
|
||||
/// <param name="Volume">Current volume of the hotspot this entity is exposed to.
|
||||
/// This is not the volume of the tile this entity is on.</param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct TileFireEvent(float Temperature, float Volume);
|
||||
@@ -4,6 +4,7 @@ using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Events;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Eye.Blinding.Systems;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.Audio;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
@@ -42,31 +42,31 @@ public sealed partial class BlockingSystem
|
||||
|
||||
private void OnUserDamageModified(EntityUid uid, BlockingUserComponent component, DamageModifyEvent args)
|
||||
{
|
||||
if (TryComp<BlockingComponent>(component.BlockingItem, out var blocking))
|
||||
if (component.BlockingItem is not { } item || !TryComp<BlockingComponent>(item, out var blocking))
|
||||
return;
|
||||
|
||||
if (args.Damage.GetTotal() <= 0)
|
||||
return;
|
||||
|
||||
// A shield should only block damage it can itself absorb. To determine that we need the Damageable component on it.
|
||||
if (!TryComp<DamageableComponent>(item, out var dmgComp))
|
||||
return;
|
||||
|
||||
var blockFraction = blocking.IsBlocking ? blocking.ActiveBlockFraction : blocking.PassiveBlockFraction;
|
||||
blockFraction = Math.Clamp(blockFraction, 0, 1);
|
||||
_damageable.TryChangeDamage((item, dmgComp), blockFraction * args.OriginalDamage);
|
||||
|
||||
var modify = new DamageModifierSet();
|
||||
foreach (var key in dmgComp.Damage.DamageDict.Keys)
|
||||
{
|
||||
if (args.Damage.GetTotal() <= 0)
|
||||
return;
|
||||
modify.Coefficients.TryAdd(key, 1 - blockFraction);
|
||||
}
|
||||
|
||||
// A shield should only block damage it can itself absorb. To determine that we need the Damageable component on it.
|
||||
if (!TryComp<DamageableComponent>(component.BlockingItem, out var dmgComp))
|
||||
return;
|
||||
args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modify);
|
||||
|
||||
var blockFraction = blocking.IsBlocking ? blocking.ActiveBlockFraction : blocking.PassiveBlockFraction;
|
||||
blockFraction = Math.Clamp(blockFraction, 0, 1);
|
||||
_damageable.TryChangeDamage(component.BlockingItem, blockFraction * args.OriginalDamage);
|
||||
|
||||
var modify = new DamageModifierSet();
|
||||
foreach (var key in dmgComp.Damage.DamageDict.Keys)
|
||||
{
|
||||
modify.Coefficients.TryAdd(key, 1 - blockFraction);
|
||||
}
|
||||
|
||||
args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modify);
|
||||
|
||||
if (blocking.IsBlocking && !args.Damage.Equals(args.OriginalDamage))
|
||||
{
|
||||
_audio.PlayPvs(blocking.BlockSound, uid);
|
||||
}
|
||||
if (blocking.IsBlocking && !args.Damage.Equals(args.OriginalDamage))
|
||||
{
|
||||
_audio.PlayPvs(blocking.BlockSound, uid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed partial class LungComponent : Component
|
||||
/// The name/key of the solution on this entity which these lungs act on.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string SolutionName = LungSystem.LungSolutionName;
|
||||
public string SolutionName = "Lung";
|
||||
|
||||
/// <summary>
|
||||
/// The solution on this entity that these lungs act on.
|
||||
|
||||
@@ -8,7 +8,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Body.Components
|
||||
{
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem), typeof(FoodSystem))]
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(StomachSystem))]
|
||||
public sealed partial class StomachComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
using BreathToolComponent = Content.Shared.Atmos.Components.BreathToolComponent;
|
||||
using InternalsComponent = Content.Shared.Body.Components.InternalsComponent;
|
||||
|
||||
namespace Content.Shared.Body.Systems;
|
||||
|
||||
@@ -15,8 +18,6 @@ public sealed class LungSystem : EntitySystem
|
||||
[Dependency] private readonly SharedInternalsSystem _internals = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
public static string LungSolutionName = "Lung";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -53,6 +54,7 @@ public sealed class LungSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: JUST METABOLIZE GASES DIRECTLY DON'T CONVERT TO REAGENTS!!! (Needs Metabolism refactor :B)
|
||||
public void GasToReagent(EntityUid uid, LungComponent lung)
|
||||
{
|
||||
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
|
||||
|
||||
@@ -6,7 +6,8 @@ using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.EntityEffects.Effects.Solution;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids;
|
||||
using Content.Shared.Forensics.Components;
|
||||
@@ -110,8 +111,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
|
||||
// bloodloss damage is based on the base value, and modified by how low your blood level is.
|
||||
var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
|
||||
|
||||
_damageableSystem.TryChangeDamage(uid, amt,
|
||||
ignoreResistances: false, interruptsDoAfters: false);
|
||||
_damageableSystem.TryChangeDamage(uid, amt, ignoreResistances: false, interruptsDoAfters: false);
|
||||
|
||||
// Apply dizziness as a symptom of bloodloss.
|
||||
// The effect is applied in a way that it will never be cleared without being healthy.
|
||||
@@ -161,7 +161,9 @@ public abstract class SharedBloodstreamSystem : EntitySystem
|
||||
{
|
||||
switch (effect)
|
||||
{
|
||||
case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream
|
||||
// TODO: Rather than this, ReactionAttempt should allow systems to remove effects from the list before the reaction.
|
||||
// TODO: I think there's a PR up on the repo for this and if there isn't I'll make one -Princess
|
||||
case EntityEffects.Effects.EntitySpawning.SpawnEntity: // Prevent entities from spawning in the bloodstream
|
||||
case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels.
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
@@ -226,7 +228,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
|
||||
|
||||
// TODO: Replace with RandomPredicted once the engine PR is merged
|
||||
// Use both the receiver and the damage causing entity for the seed so that we have different results for multiple attacks in the same tick
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0 });
|
||||
var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0 );
|
||||
var rand = new System.Random(seed);
|
||||
var prob = Math.Clamp(totalFloat / 25, 0, 1);
|
||||
if (totalFloat > 0 && rand.Prob(prob))
|
||||
|
||||
@@ -181,7 +181,7 @@ public partial class SharedBodySystem
|
||||
{
|
||||
// TODO BODY SYSTEM KILL : remove this when wounding and required parts are implemented properly
|
||||
var damage = new DamageSpecifier(Prototypes.Index(BloodlossDamageType), 300);
|
||||
Damageable.TryChangeDamage(bodyEnt, damage);
|
||||
Damageable.ChangeDamage(bodyEnt.Owner, damage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Standing;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Internals;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -258,11 +259,15 @@ public abstract class SharedInternalsSystem : EntitySystem
|
||||
Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
|
||||
{
|
||||
// TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
|
||||
// Prioritise
|
||||
// 1. back equipped tanks
|
||||
// 2. exo-slot tanks
|
||||
// 3. in-hand tanks
|
||||
// 4. pocket/belt tanks
|
||||
// Lookup order:
|
||||
// 1. Back
|
||||
// 2. Exo-slot
|
||||
// 3. In-hand
|
||||
// 4. Pocket/belt
|
||||
// Jetpacks will only be used as a fallback if no other tank is found
|
||||
|
||||
// Store the first jetpack seen
|
||||
Entity<GasTankComponent>? found = null;
|
||||
|
||||
if (!Resolve(user, ref user.Comp2, ref user.Comp3))
|
||||
return null;
|
||||
@@ -271,22 +276,36 @@ public abstract class SharedInternalsSystem : EntitySystem
|
||||
TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
|
||||
_gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
|
||||
{
|
||||
return (backEntity.Value, backGasTank);
|
||||
found = (backEntity.Value, backGasTank);
|
||||
if (!HasComp<JetpackComponent>(backEntity.Value))
|
||||
{
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
|
||||
TryComp<GasTankComponent>(entity, out var gasTank) &&
|
||||
_gasTank.CanConnectToInternals((entity.Value, gasTank)))
|
||||
{
|
||||
return (entity.Value, gasTank);
|
||||
found ??= (entity.Value, gasTank);
|
||||
if (!HasComp<JetpackComponent>(entity.Value))
|
||||
{
|
||||
return (entity.Value, gasTank);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
|
||||
{
|
||||
if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
|
||||
return (item, gasTank);
|
||||
{
|
||||
found ??= (item, gasTank);
|
||||
if (!HasComp<JetpackComponent>(item))
|
||||
{
|
||||
return (item, gasTank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ public abstract partial class SharedBuckleSystem
|
||||
// TODO: This is doing 4 moveevents this is why I left the warning in, if you're going to remove it make it only do 1 moveevent.
|
||||
if (strap.Comp.BuckleOffset != Vector2.Zero)
|
||||
{
|
||||
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.BuckleOffset);
|
||||
_transform.SetCoordinates(buckle, buckleXform, oldBuckledXform.Coordinates.Offset(strap.Comp.BuckleOffset));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class ItemCabinetSystem : EntitySystem
|
||||
private void OnMapInit(Entity<ItemCabinetComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
// update at mapinit to avoid copy pasting locked: true and locked: false for each closed/open prototype
|
||||
SetSlotLock(ent, !_openable.IsOpen(ent));
|
||||
SetSlotLock(ent, _openable.IsClosed(ent, null));
|
||||
}
|
||||
|
||||
private void UpdateAppearance(Entity<ItemCabinetComponent> ent)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -55,6 +56,151 @@ public abstract class SharedCargoSystem : EntitySystem
|
||||
}
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about the given bank account.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get bank account info from.</param>
|
||||
/// <param name="accountPrototypeId">Bank account prototype ID to get info for.</param>
|
||||
/// <param name="money">The amount of money in the account</param>
|
||||
/// <returns>Whether or not the bank account exists.</returns>
|
||||
public bool TryGetAccount(Entity<StationBankAccountComponent?> station, ProtoId<CargoAccountPrototype> accountPrototypeId, out int money)
|
||||
{
|
||||
money = 0;
|
||||
|
||||
if (!Resolve(station, ref station.Comp))
|
||||
return false;
|
||||
|
||||
return station.Comp.Accounts.TryGetValue(accountPrototypeId, out money);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a readonly dictionary of all accounts and their money info.
|
||||
/// </summary>
|
||||
/// <param name="station">Station to get bank account info from.</param>
|
||||
/// <returns>Whether or not the bank account exists.</returns>
|
||||
public IReadOnlyDictionary<ProtoId<CargoAccountPrototype>, int> GetAccounts(Entity<StationBankAccountComponent?> station)
|
||||
{
|
||||
if (!Resolve(station, ref station.Comp))
|
||||
return new Dictionary<ProtoId<CargoAccountPrototype>, int>();
|
||||
|
||||
return station.Comp.Accounts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to adjust the money of a certain bank account.
|
||||
/// </summary>
|
||||
/// <param name="station">Station where the bank account is from</param>
|
||||
/// <param name="accountPrototypeId">the id of the bank account</param>
|
||||
/// <param name="money">how much money to set the account to</param>
|
||||
/// <param name="createAccount">Whether or not it should create the account if it doesn't exist.</param>
|
||||
/// <param name="dirty">Whether to mark the bank account component as dirty.</param>
|
||||
/// <returns>Whether or not setting the value succeeded.</returns>
|
||||
public bool TryAdjustBankAccount(
|
||||
Entity<StationBankAccountComponent?> station,
|
||||
ProtoId<CargoAccountPrototype> accountPrototypeId,
|
||||
int money,
|
||||
bool createAccount = false,
|
||||
bool dirty = true)
|
||||
{
|
||||
if (!Resolve(station, ref station.Comp))
|
||||
return false;
|
||||
|
||||
var accounts = station.Comp.Accounts;
|
||||
|
||||
if (!accounts.ContainsKey(accountPrototypeId) && !createAccount)
|
||||
return false;
|
||||
|
||||
accounts[accountPrototypeId] += money;
|
||||
var ev = new BankBalanceUpdatedEvent(station, station.Comp.Accounts);
|
||||
RaiseLocalEvent(station, ref ev, true);
|
||||
|
||||
if (!dirty)
|
||||
return true;
|
||||
|
||||
Dirty(station);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set the money of a certain bank account.
|
||||
/// </summary>
|
||||
/// <param name="station">Station where the bank account is from</param>
|
||||
/// <param name="accountPrototypeId">the id of the bank account</param>
|
||||
/// <param name="money">how much money to set the account to</param>
|
||||
/// <param name="createAccount">Whether or not it should create the account if it doesn't exist.</param>
|
||||
/// <param name="dirty">Whether to mark the bank account component as dirty.</param>
|
||||
/// <returns>Whether or not setting the value succeeded.</returns>
|
||||
public bool TrySetBankAccount(
|
||||
Entity<StationBankAccountComponent?> station,
|
||||
ProtoId<CargoAccountPrototype> accountPrototypeId,
|
||||
int money,
|
||||
bool createAccount = false,
|
||||
bool dirty = true)
|
||||
{
|
||||
if (!Resolve(station, ref station.Comp))
|
||||
return false;
|
||||
|
||||
var accounts = station.Comp.Accounts;
|
||||
|
||||
if (!accounts.ContainsKey(accountPrototypeId) && !createAccount)
|
||||
return false;
|
||||
|
||||
accounts[accountPrototypeId] = money;
|
||||
var ev = new BankBalanceUpdatedEvent(station, station.Comp.Accounts);
|
||||
RaiseLocalEvent(station, ref ev, true);
|
||||
|
||||
if (!dirty)
|
||||
return true;
|
||||
|
||||
Dirty(station);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateBankAccount(
|
||||
Entity<StationBankAccountComponent?> ent,
|
||||
int balanceAdded,
|
||||
ProtoId<CargoAccountPrototype> account,
|
||||
bool dirty = true)
|
||||
{
|
||||
UpdateBankAccount(
|
||||
ent,
|
||||
balanceAdded,
|
||||
new Dictionary<ProtoId<CargoAccountPrototype>, double> { {account, 1} },
|
||||
dirty: dirty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or removes funds from the <see cref="StationBankAccountComponent"/>.
|
||||
/// </summary>
|
||||
/// <param name="ent">The station.</param>
|
||||
/// <param name="balanceAdded">The amount of funds to add or remove.</param>
|
||||
/// <param name="accountDistribution">The distribution between individual <see cref="CargoAccountPrototype"/>.</param>
|
||||
/// <param name="dirty">Whether to mark the bank account component as dirty.</param>
|
||||
[PublicAPI]
|
||||
public void UpdateBankAccount(
|
||||
Entity<StationBankAccountComponent?> ent,
|
||||
int balanceAdded,
|
||||
Dictionary<ProtoId<CargoAccountPrototype>, double> accountDistribution,
|
||||
bool dirty = true)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
|
||||
foreach (var (account, percent) in accountDistribution)
|
||||
{
|
||||
var accountBalancedAdded = (int) Math.Round(percent * balanceAdded);
|
||||
ent.Comp.Accounts[account] += accountBalancedAdded;
|
||||
}
|
||||
|
||||
var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Accounts);
|
||||
RaiseLocalEvent(ent, ref ev, true);
|
||||
|
||||
if (!dirty)
|
||||
return;
|
||||
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using Content.Shared.Charges.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Changeling.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Changeling transformation in item form!
|
||||
/// An entity with this component works like an implanter:
|
||||
/// First you use it on a humanoid to make a copy of their identity, along with all species relevant components,
|
||||
/// then use it on someone else to tranform them into a clone of them.
|
||||
/// Can be used in combination with <see cref="LimitedChargesComponent"/>
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class ChangelingClonerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// A clone of the player you have copied the identity from.
|
||||
/// This is a full humanoid backup, stored on a paused map.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Since this entity is stored on a separate map it will be outside PVS range.
|
||||
/// </remarks>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? ClonedBackup;
|
||||
|
||||
/// <summary>
|
||||
/// Current state of the item.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public ChangelingClonerState State = ChangelingClonerState.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The cloning settings to use.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public ProtoId<CloningSettingsPrototype> Settings = "ChangelingCloningSettings";
|
||||
|
||||
/// <summary>
|
||||
/// Doafter time for drawing and injecting.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan DoAfter = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Can this item be used more than once?
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Reusable = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to add a reset verb to purge the stored identity,
|
||||
/// allowing you to draw a new one.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool CanReset = true;
|
||||
|
||||
/// <summary>
|
||||
/// Raise events when renaming the target?
|
||||
/// This will change their ID card, crew manifest entry, and so on.
|
||||
/// For admeme purposes.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool RaiseNameChangeEvents;
|
||||
|
||||
/// <summary>
|
||||
/// The sound to play when taking someone's identity with the item.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier? DrawSound;
|
||||
|
||||
/// <summary>
|
||||
/// The sound to play when someone is transformed.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier? InjectSound;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current state of the item.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum ChangelingClonerState : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// No sample taken yet.
|
||||
/// </summary>
|
||||
Empty,
|
||||
/// <summary>
|
||||
/// Filled with a DNA sample.
|
||||
/// </summary>
|
||||
Filled,
|
||||
/// <summary>
|
||||
/// Has been used (single use only).
|
||||
/// </summary>
|
||||
Spent,
|
||||
}
|
||||
308
Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs
Normal file
308
Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Forensics.Systems;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Changeling.Systems;
|
||||
|
||||
public sealed class ChangelingClonerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedCloningSystem _cloning = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!;
|
||||
[Dependency] private readonly SharedForensicsSystem _forensics = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ChangelingClonerComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<ChangelingClonerComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
|
||||
SubscribeLocalEvent<ChangelingClonerComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<ChangelingClonerComponent, ClonerDrawDoAfterEvent>(OnDraw);
|
||||
SubscribeLocalEvent<ChangelingClonerComponent, ClonerInjectDoAfterEvent>(OnInject);
|
||||
SubscribeLocalEvent<ChangelingClonerComponent, ComponentShutdown>(OnShutDown);
|
||||
}
|
||||
|
||||
private void OnShutDown(Entity<ChangelingClonerComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
// Delete the stored clone.
|
||||
PredictedQueueDel(ent.Comp.ClonedBackup);
|
||||
}
|
||||
|
||||
private void OnExamine(Entity<ChangelingClonerComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
var msg = ent.Comp.State switch
|
||||
{
|
||||
ChangelingClonerState.Empty => "changeling-cloner-component-empty",
|
||||
ChangelingClonerState.Filled => "changeling-cloner-component-filled",
|
||||
ChangelingClonerState.Spent => "changeling-cloner-component-spent",
|
||||
_ => "error"
|
||||
};
|
||||
|
||||
args.PushMarkup(Loc.GetString(msg));
|
||||
|
||||
}
|
||||
|
||||
private void OnGetVerbs(Entity<ChangelingClonerComponent> ent, ref GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess || args.Hands == null)
|
||||
return;
|
||||
|
||||
if (!ent.Comp.CanReset || ent.Comp.State == ChangelingClonerState.Spent)
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
args.Verbs.Add(new Verb
|
||||
{
|
||||
Text = Loc.GetString("changeling-cloner-component-reset-verb"),
|
||||
Disabled = ent.Comp.ClonedBackup == null,
|
||||
Act = () => Reset(ent.AsNullable(), user),
|
||||
DoContactInteraction = true,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnAfterInteract(Entity<ChangelingClonerComponent> ent, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach || args.Target == null)
|
||||
return;
|
||||
|
||||
switch (ent.Comp.State)
|
||||
{
|
||||
case ChangelingClonerState.Empty:
|
||||
args.Handled |= TryDraw(ent.AsNullable(), args.Target.Value, args.User);
|
||||
break;
|
||||
case ChangelingClonerState.Filled:
|
||||
args.Handled |= TryInject(ent.AsNullable(), args.Target.Value, args.User);
|
||||
break;
|
||||
case ChangelingClonerState.Spent:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnDraw(Entity<ChangelingClonerComponent> ent, ref ClonerDrawDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Target == null)
|
||||
return;
|
||||
|
||||
Draw(ent.AsNullable(), args.Target.Value, args.User);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnInject(Entity<ChangelingClonerComponent> ent, ref ClonerInjectDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Target == null)
|
||||
return;
|
||||
|
||||
Inject(ent.AsNullable(), args.Target.Value, args.User);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a DoAfter to draw a DNA sample from the target.
|
||||
/// </summary>
|
||||
public bool TryDraw(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
if (ent.Comp.State != ChangelingClonerState.Empty)
|
||||
return false;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
return false; // cloning only works for humanoids at the moment
|
||||
|
||||
var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerDrawDoAfterEvent(), ent, target: target, used: ent)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
};
|
||||
|
||||
if (!_doAfter.TryStartDoAfter(args))
|
||||
return false;
|
||||
|
||||
var userIdentity = Identity.Entity(user, EntityManager);
|
||||
var targetIdentity = Identity.Entity(target, EntityManager);
|
||||
var userMsg = Loc.GetString("changeling-cloner-component-draw-user", ("user", userIdentity), ("target", targetIdentity));
|
||||
var targetMsg = Loc.GetString("changeling-cloner-component-draw-target", ("user", userIdentity), ("target", targetIdentity));
|
||||
_popup.PopupClient(userMsg, target, user);
|
||||
|
||||
if (user != target) // don't show the warning if using the item on yourself
|
||||
_popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a DoAfter to inject a DNA sample into someone, turning them into a clone of the original.
|
||||
/// </summary>
|
||||
public bool TryInject(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
if (ent.Comp.State != ChangelingClonerState.Filled)
|
||||
return false;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
return false; // cloning only works for humanoids at the moment
|
||||
|
||||
var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerInjectDoAfterEvent(), ent, target: target, used: ent)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
};
|
||||
|
||||
if (!_doAfter.TryStartDoAfter(args))
|
||||
return false;
|
||||
|
||||
var userIdentity = Identity.Entity(user, EntityManager);
|
||||
var targetIdentity = Identity.Entity(target, EntityManager);
|
||||
var userMsg = Loc.GetString("changeling-cloner-component-inject-user", ("user", userIdentity), ("target", targetIdentity));
|
||||
var targetMsg = Loc.GetString("changeling-cloner-component-inject-target", ("user", userIdentity), ("target", targetIdentity));
|
||||
_popup.PopupClient(userMsg, target, user);
|
||||
|
||||
if (user != target) // don't show the warning if using the item on yourself
|
||||
_popup.PopupEntity(targetMsg, user, target, PopupType.LargeCaution);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw a DNA sample from the target.
|
||||
/// This will create a clone stored on a paused map for data storage.
|
||||
/// </summary>
|
||||
public void Draw(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
|
||||
if (ent.Comp.State != ChangelingClonerState.Empty)
|
||||
return;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
return; // cloning only works for humanoids at the moment
|
||||
|
||||
if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
|
||||
return;
|
||||
|
||||
_adminLogger.Add(LogType.Identity,
|
||||
$"{user} is using {ent.Owner} to draw DNA from {target}.");
|
||||
|
||||
// Make a copy of the target on a paused map, so that we can apply their components later.
|
||||
ent.Comp.ClonedBackup = _changelingIdentity.CloneToPausedMap(settings, target);
|
||||
ent.Comp.State = ChangelingClonerState.Filled;
|
||||
_appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Filled);
|
||||
Dirty(ent);
|
||||
|
||||
_audio.PlayPredicted(ent.Comp.DrawSound, target, user);
|
||||
_forensics.TransferDna(ent, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inject a DNA sample into someone, turning them into a clone of the original.
|
||||
/// </summary>
|
||||
public void Inject(Entity<ChangelingClonerComponent?> ent, EntityUid target, EntityUid user)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
|
||||
if (ent.Comp.State != ChangelingClonerState.Filled)
|
||||
return;
|
||||
|
||||
if (!HasComp<HumanoidAppearanceComponent>(target))
|
||||
return; // cloning only works for humanoids at the moment
|
||||
|
||||
if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
|
||||
return;
|
||||
|
||||
_audio.PlayPredicted(ent.Comp.InjectSound, target, user);
|
||||
_forensics.TransferDna(ent, target); // transfer DNA before overwriting it
|
||||
|
||||
if (!ent.Comp.Reusable)
|
||||
{
|
||||
ent.Comp.State = ChangelingClonerState.Spent;
|
||||
_appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Spent);
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
if (!Exists(ent.Comp.ClonedBackup))
|
||||
return; // the entity is likely out of PVS range on the client
|
||||
|
||||
_adminLogger.Add(LogType.Identity,
|
||||
$"{user} is using {ent.Owner} to inject DNA into {target} changing their identity to {ent.Comp.ClonedBackup.Value}.");
|
||||
|
||||
// Do the actual transformation.
|
||||
_humanoidAppearance.CloneAppearance(ent.Comp.ClonedBackup.Value, target);
|
||||
_cloning.CloneComponents(ent.Comp.ClonedBackup.Value, target, settings);
|
||||
_metaData.SetEntityName(target, Name(ent.Comp.ClonedBackup.Value), raiseEvents: ent.Comp.RaiseNameChangeEvents);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Purge the stored DNA and allow to draw again.
|
||||
/// </summary>
|
||||
public void Reset(Entity<ChangelingClonerComponent?> ent, EntityUid? user)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
|
||||
// Delete the stored clone.
|
||||
PredictedQueueDel(ent.Comp.ClonedBackup);
|
||||
ent.Comp.ClonedBackup = null;
|
||||
ent.Comp.State = ChangelingClonerState.Empty;
|
||||
_appearance.SetData(ent.Owner, ChangelingClonerVisuals.State, ChangelingClonerState.Empty);
|
||||
Dirty(ent);
|
||||
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("changeling-cloner-component-reset-popup"), user.Value, user.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Doafter event for drawing a DNA sample.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ClonerDrawDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
/// <summary>
|
||||
/// DoAfterEvent for injecting a DNA sample, turning a player into someone else.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class ClonerInjectDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Key for the generic visualizer.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum ChangelingClonerVisuals : byte
|
||||
{
|
||||
State,
|
||||
}
|
||||
@@ -4,7 +4,8 @@ using Content.Shared.Armor;
|
||||
using Content.Shared.Atmos.Rotting;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Humanoid;
|
||||
@@ -92,7 +93,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
||||
if (damage.Damage.DamageDict.TryGetValue(damagePoints.Key, out var val) && val > comp.DevourConsumeDamageCap)
|
||||
return;
|
||||
}
|
||||
_damageable.TryChangeDamage(target, comp.DamagePerTick, true, true, damage, user);
|
||||
_damageable.ChangeDamage((target.Value, damage), comp.DamagePerTick, true, true, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -83,20 +83,19 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone a target humanoid into nullspace and add it to the Changelings list of identities.
|
||||
/// It creates a perfect copy of the target and can be used to pull components down for future use
|
||||
/// Clone a target humanoid to a paused map.
|
||||
/// It creates a perfect copy of the target and can be used to pull components down for future use.
|
||||
/// </summary>
|
||||
/// <param name="ent">the Changeling</param>
|
||||
/// <param name="target">the targets uid</param>
|
||||
public EntityUid? CloneToPausedMap(Entity<ChangelingIdentityComponent> ent, EntityUid target)
|
||||
/// <param name="settings">The settings to use for cloning.</param>
|
||||
/// <param name="target">The target to clone.</param>
|
||||
public EntityUid? CloneToPausedMap(CloningSettingsPrototype settings, EntityUid target)
|
||||
{
|
||||
// Don't create client side duplicate clones or a clientside map.
|
||||
if (_net.IsClient)
|
||||
return null;
|
||||
|
||||
if (!TryComp<HumanoidAppearanceComponent>(target, out var humanoid)
|
||||
|| !_prototype.Resolve(humanoid.Species, out var speciesPrototype)
|
||||
|| !_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings))
|
||||
|| !_prototype.Resolve(humanoid.Species, out var speciesPrototype))
|
||||
return null;
|
||||
|
||||
EnsurePausedMap();
|
||||
@@ -117,10 +116,30 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
|
||||
|
||||
var targetName = _nameMod.GetBaseName(target);
|
||||
_metaSystem.SetEntityName(clone, targetName);
|
||||
ent.Comp.ConsumedIdentities.Add(clone);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone a target humanoid to a paused map and add it to the Changelings list of identities.
|
||||
/// It creates a perfect copy of the target and can be used to pull components down for future use.
|
||||
/// </summary>
|
||||
/// <param name="ent">The Changeling.</param>
|
||||
/// <param name="target">The target to clone.</param>
|
||||
public EntityUid? CloneToPausedMap(Entity<ChangelingIdentityComponent> ent, EntityUid target)
|
||||
{
|
||||
if (!_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings))
|
||||
return null;
|
||||
|
||||
var clone = CloneToPausedMap(settings, target);
|
||||
|
||||
if (clone == null)
|
||||
return null;
|
||||
|
||||
ent.Comp.ConsumedIdentities.Add(clone.Value);
|
||||
|
||||
Dirty(ent);
|
||||
HandlePvsOverride(ent, clone);
|
||||
HandlePvsOverride(ent, clone.Value);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Actions.Events;
|
||||
using Content.Shared.Charges.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -19,7 +20,7 @@ public abstract class SharedChargesSystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<LimitedChargesComponent, ExaminedEvent>(OnExamine);
|
||||
|
||||
SubscribeLocalEvent<LimitedChargesComponent, RejuvenateEvent>(OnRejuvenate);
|
||||
SubscribeLocalEvent<LimitedChargesComponent, ActionAttemptEvent>(OnChargesAttempt);
|
||||
SubscribeLocalEvent<LimitedChargesComponent, MapInitEvent>(OnChargesMapInit);
|
||||
SubscribeLocalEvent<LimitedChargesComponent, ActionPerformedEvent>(OnChargesPerformed);
|
||||
@@ -48,6 +49,11 @@ public abstract class SharedChargesSystem : EntitySystem
|
||||
args.PushMarkup(Loc.GetString("limited-charges-recharging", ("seconds", timeRemaining.TotalSeconds.ToString("F1"))));
|
||||
}
|
||||
|
||||
private void OnRejuvenate(Entity<LimitedChargesComponent> ent, ref RejuvenateEvent args)
|
||||
{
|
||||
ResetCharges(ent.AsNullable());
|
||||
}
|
||||
|
||||
private void OnChargesAttempt(Entity<LimitedChargesComponent> ent, ref ActionAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// An event raised just before an emote is performed, providing systems with an opportunity to cancel the emote's performance.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public sealed class BeforeEmoteEvent(EntityUid source, EmotePrototype emote)
|
||||
: CancellableEntityEventArgs, IInventoryRelayEvent
|
||||
{
|
||||
public readonly EntityUid Source = source;
|
||||
public readonly EmotePrototype Emote = emote;
|
||||
|
||||
/// <summary>
|
||||
/// The equipment that is blocking emoting. Should only be non-null if the event was canceled.
|
||||
/// </summary>
|
||||
public EntityUid? Blocker = null;
|
||||
|
||||
public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised by the chat system when an entity made some emote.
|
||||
/// Use it to play sound, change sprite or something else.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct EmoteEvent(EmotePrototype Emote)
|
||||
{
|
||||
/// <summary>
|
||||
/// The used emote.
|
||||
/// </summary>
|
||||
public EmotePrototype Emote = Emote;
|
||||
|
||||
/// <summary>
|
||||
/// If this message has already been "handled" by a previous system.
|
||||
/// </summary>
|
||||
public bool Handled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent by the client when requesting the server to play a specific emote selected from the emote radial menu.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PlayEmoteMessage(ProtoId<EmotePrototype> protoId) : EntityEventArgs
|
||||
{
|
||||
|
||||
@@ -3,6 +3,28 @@ namespace Content.Shared.Chat;
|
||||
public interface ISharedChatManager
|
||||
{
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Send an admin alert to the admin chat channel.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to send.</param>
|
||||
void SendAdminAlert(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Send an admin alert to the admin chat channel specifically about the given player.
|
||||
/// Will include info extra like their antag status and name.
|
||||
/// </summary>
|
||||
/// <param name="player">The player that the message is about.</param>
|
||||
/// <param name="message">The message to send.</param>
|
||||
void SendAdminAlert(EntityUid player, string message);
|
||||
|
||||
/// <summary>
|
||||
/// This is a dangerous function! Only pass in property escaped text.
|
||||
/// See: <see cref="SendAdminAlert(string)"/>
|
||||
/// <br/><br/>
|
||||
/// Use this for things that need to be unformatted (like tpto links) but ensure that everything else
|
||||
/// is formated properly. If it's not, players could sneak in ban links or other nasty commands that the admins
|
||||
/// could clink on.
|
||||
/// </summary>
|
||||
void SendAdminAlertNoFormatOrEscape(string message);
|
||||
}
|
||||
|
||||
@@ -14,30 +14,44 @@ public sealed partial class AutoEmotePrototype : IPrototype
|
||||
/// The ID of the emote prototype.
|
||||
/// </summary>
|
||||
[DataField("emote", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
|
||||
public string EmoteId = String.Empty;
|
||||
public string EmoteId = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// How often an attempt at the emote will be made.
|
||||
/// </summary>
|
||||
[DataField("interval", required: true)]
|
||||
[DataField(required: true)]
|
||||
public TimeSpan Interval;
|
||||
|
||||
/// <summary>
|
||||
/// Probability of performing the emote each interval.
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
[DataField("chance")]
|
||||
public float Chance = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Also send the emote in chat.
|
||||
/// <summary>
|
||||
[DataField("withChat")]
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool WithChat = true;
|
||||
|
||||
/// <summary>
|
||||
/// Should we ignore action blockers?
|
||||
/// This does nothing if WithChat is false.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool IgnoreActionBlocker;
|
||||
|
||||
/// <summary>
|
||||
/// Should we ignore whitelists and force the emote?
|
||||
/// This does nothing if WithChat is false.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Force;
|
||||
|
||||
/// <summary>
|
||||
/// Hide the chat message from the chat window, only showing the popup.
|
||||
/// This does nothing if WithChat is false.
|
||||
/// <summary>
|
||||
[DataField("hiddenFromChatWindow")]
|
||||
public bool HiddenFromChatWindow = false;
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool HiddenFromChatWindow;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Shared.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// This event should be sent everytime an entity talks (Radio, local chat, etc...).
|
||||
/// The event is sent to both the entity itself, and all clothing (For stuff like voice masks).
|
||||
/// This event should be sent everytime an entity talks (Radio, local chat, etc...).
|
||||
/// The event is sent to both the entity itself, and all clothing (For stuff like voice masks).
|
||||
/// </summary>
|
||||
public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelayEvent
|
||||
{
|
||||
@@ -22,3 +23,54 @@ public sealed class TransformSpeakerNameEvent : EntityEventArgs, IInventoryRelay
|
||||
SpeechVerb = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised broadcast in order to transform speech.transmit
|
||||
/// </summary>
|
||||
public sealed class TransformSpeechEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Sender;
|
||||
public string Message;
|
||||
|
||||
public TransformSpeechEvent(EntityUid sender, string message)
|
||||
{
|
||||
Sender = sender;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Sender;
|
||||
public bool IgnoreBlocker;
|
||||
|
||||
public CheckIgnoreSpeechBlockerEvent(EntityUid sender, bool ignoreBlocker)
|
||||
{
|
||||
Sender = sender;
|
||||
IgnoreBlocker = ignoreBlocker;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity when it speaks, either through 'say' or 'whisper'.
|
||||
/// </summary>
|
||||
public sealed class EntitySpokeEvent : EntityEventArgs
|
||||
{
|
||||
public readonly EntityUid Source;
|
||||
public readonly string Message;
|
||||
public readonly string? ObfuscatedMessage; // not null if this was a whisper
|
||||
|
||||
/// <summary>
|
||||
/// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio
|
||||
/// message gets sent on this channel, this should be set to null to prevent duplicate messages.
|
||||
/// </summary>
|
||||
public RadioChannelPrototype? Channel;
|
||||
|
||||
public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage)
|
||||
{
|
||||
Source = source;
|
||||
Message = message;
|
||||
Channel = channel;
|
||||
ObfuscatedMessage = obfuscatedMessage;
|
||||
}
|
||||
}
|
||||
|
||||
276
Content.Shared/Chat/SharedChatSystem.Emote.cs
Normal file
276
Content.Shared/Chat/SharedChatSystem.Emote.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using System.Collections.Frozen;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Shared.Chat;
|
||||
|
||||
public abstract partial class SharedChatSystem
|
||||
{
|
||||
private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
|
||||
|
||||
private void CacheEmotes()
|
||||
{
|
||||
var dict = new Dictionary<string, EmotePrototype>();
|
||||
var emotes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
|
||||
foreach (var emote in emotes)
|
||||
{
|
||||
foreach (var word in emote.ChatTriggers)
|
||||
{
|
||||
var lowerWord = word.ToLower();
|
||||
if (dict.TryGetValue(lowerWord, out var value))
|
||||
{
|
||||
var errMsg = $"Duplicate of emote word {lowerWord} in emotes {emote.ID} and {value.ID}";
|
||||
Log.Error(errMsg);
|
||||
continue;
|
||||
}
|
||||
|
||||
dict.Add(lowerWord, emote);
|
||||
}
|
||||
}
|
||||
|
||||
_wordEmoteDict = dict.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the selected entity emote using the given <see cref="EmotePrototype"/> and sends a message to chat.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity that is speaking</param>
|
||||
/// <param name="emoteId">The id of emote prototype. Should have valid <see cref="EmotePrototype.ChatMessages"/></param>
|
||||
/// <param name="hideLog">Whether this message should appear in the adminlog window, or not.</param>
|
||||
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
|
||||
/// <param name="ignoreActionBlocker">Whether emote action blocking should be ignored or not.</param>
|
||||
/// <param name="nameOverride">
|
||||
/// The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>.
|
||||
/// If this is set, the event will not get raised.
|
||||
/// </param>
|
||||
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
|
||||
/// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithChat(
|
||||
EntityUid source,
|
||||
string emoteId,
|
||||
ChatTransmitRange range = ChatTransmitRange.Normal,
|
||||
bool hideLog = false,
|
||||
string? nameOverride = null,
|
||||
bool ignoreActionBlocker = false,
|
||||
bool forceEmote = false
|
||||
)
|
||||
{
|
||||
if (!_prototypeManager.Resolve<EmotePrototype>(emoteId, out var proto))
|
||||
return false;
|
||||
|
||||
return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the selected entity emote using the given <see cref="EmotePrototype"/> and sends a message to chat.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity that is speaking.</param>
|
||||
/// <param name="emote">The emote prototype. Should have valid <see cref="EmotePrototype.ChatMessages"/>.</param>
|
||||
/// <param name="hideLog">Whether this message should appear in the adminlog window or not.</param>
|
||||
/// <param name="ignoreActionBlocker">Whether emote action blocking should be ignored or not.</param>
|
||||
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
|
||||
/// <param name="nameOverride">
|
||||
/// The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>.
|
||||
/// If this is set, the event will not get raised.
|
||||
/// </param>
|
||||
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
|
||||
/// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithChat(
|
||||
EntityUid source,
|
||||
EmotePrototype emote,
|
||||
ChatTransmitRange range = ChatTransmitRange.Normal,
|
||||
bool hideLog = false,
|
||||
string? nameOverride = null,
|
||||
bool ignoreActionBlocker = false,
|
||||
bool forceEmote = false
|
||||
)
|
||||
{
|
||||
if (!forceEmote && !AllowedToUseEmote(source, emote))
|
||||
return false;
|
||||
|
||||
var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
|
||||
|
||||
// check if proto has valid message for chat
|
||||
if (didEmote && emote.ChatMessages.Count != 0)
|
||||
{
|
||||
// not all emotes are loc'd, but for the ones that are we pass in entity
|
||||
var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
|
||||
SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
|
||||
}
|
||||
|
||||
return didEmote;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the selected entity emote using the given <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||
/// </summary>
|
||||
/// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
|
||||
{
|
||||
if (!_prototypeManager.Resolve<EmotePrototype>(emoteId, out var proto))
|
||||
return false;
|
||||
|
||||
return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the selected entity emote using the given <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||
/// </summary>
|
||||
/// <returns>True if an emote was performed. False if the emote is unavailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
|
||||
{
|
||||
if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
|
||||
return false;
|
||||
|
||||
return TryInvokeEmoteEvent(uid, proto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find and play the relevant emote sound in an emote sounds collection.
|
||||
/// </summary>
|
||||
/// <returns>True if emote sound was played.</returns>
|
||||
public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote, AudioParams? audioParams = null)
|
||||
{
|
||||
return TryPlayEmoteSound(uid, proto, emote.ID, audioParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find and play the relevant emote sound in an emote sounds collection.
|
||||
/// </summary>
|
||||
/// <returns>True if emote sound was played.</returns>
|
||||
public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId, AudioParams? audioParams = null)
|
||||
{
|
||||
if (proto == null)
|
||||
return false;
|
||||
|
||||
// try to get specific sound for this emote
|
||||
if (!proto.Sounds.TryGetValue(emoteId, out var sound))
|
||||
{
|
||||
// no specific sound - check fallback
|
||||
sound = proto.FallbackSound;
|
||||
if (sound == null)
|
||||
return false;
|
||||
}
|
||||
|
||||
// optional override params > general params for all sounds in set > individual sound params
|
||||
var param = audioParams ?? proto.GeneralParams ?? sound.Params;
|
||||
_audio.PlayPvs(sound, uid, param);
|
||||
return true;
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks if a valid emote was typed, to play sounds and etc and invokes an event.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity that is speaking</param>
|
||||
/// <param name="textInput">Formatted emote message.</param>
|
||||
/// <returns>True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be.</returns>
|
||||
protected bool TryEmoteChatInput(EntityUid source, string textInput)
|
||||
{
|
||||
var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
|
||||
if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
|
||||
return true;
|
||||
|
||||
if (!AllowedToUseEmote(source, emote))
|
||||
return true;
|
||||
|
||||
return TryInvokeEmoteEvent(source, emote);
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Checks if we can use this emote based on the emotes whitelist, blacklist, and availability to the entity.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity that is speaking</param>
|
||||
/// <param name="emote">The emote being used</param>
|
||||
public bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
|
||||
{
|
||||
// If emote is in AllowedEmotes, it will bypass whitelist and blacklist
|
||||
if (TryComp<SpeechComponent>(source, out var speech) &&
|
||||
speech.AllowedEmotes.Contains(emote.ID))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the whitelist and blacklist
|
||||
if (_whitelist.IsWhitelistFail(emote.Whitelist, source) ||
|
||||
_whitelist.IsBlacklistPass(emote.Blacklist, source))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the emote is available for all
|
||||
if (!emote.Available)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
|
||||
/// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player
|
||||
/// explaining why the emote didn't happen.
|
||||
/// </summary>
|
||||
/// <param name="uid">The entity which is emoting</param>
|
||||
/// <param name="proto">The emote which is being performed</param>
|
||||
/// <returns>True if the emote was performed, false otherwise.</returns>
|
||||
private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
|
||||
{
|
||||
var beforeEv = new BeforeEmoteEvent(uid, proto);
|
||||
RaiseLocalEvent(uid, ref beforeEv);
|
||||
|
||||
if (beforeEv.Cancelled)
|
||||
{
|
||||
// Chat is not predicted anyways, so no need to predict this popup either.
|
||||
if (_net.IsClient)
|
||||
return false;
|
||||
|
||||
if (beforeEv.Blocker != null)
|
||||
{
|
||||
_popup.PopupEntity(
|
||||
Loc.GetString(
|
||||
"chat-system-emote-cancelled-blocked",
|
||||
("emote", Loc.GetString(proto.Name).ToLower()),
|
||||
("blocker", beforeEv.Blocker.Value)
|
||||
),
|
||||
uid,
|
||||
uid
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(
|
||||
Loc.GetString("chat-system-emote-cancelled-generic",
|
||||
("emote", Loc.GetString(proto.Name).ToLower())),
|
||||
uid,
|
||||
uid
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var ev = new EmoteEvent(proto);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string TrimPunctuation(string textInput)
|
||||
{
|
||||
var trimEnd = textInput.Length;
|
||||
while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1]))
|
||||
{
|
||||
trimEnd--;
|
||||
}
|
||||
|
||||
var trimStart = 0;
|
||||
while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart]))
|
||||
{
|
||||
trimStart++;
|
||||
}
|
||||
|
||||
return textInput[trimStart..trimEnd];
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Chat;
|
||||
|
||||
public abstract class SharedChatSystem : EntitySystem
|
||||
public abstract partial class SharedChatSystem : EntitySystem
|
||||
{
|
||||
public const char RadioCommonPrefix = ';';
|
||||
public const char RadioChannelPrefix = ':';
|
||||
@@ -38,6 +46,11 @@ public abstract class SharedChatSystem : EntitySystem
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Cache of the keycodes for faster lookup.
|
||||
@@ -47,15 +60,21 @@ public abstract class SharedChatSystem : EntitySystem
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel));
|
||||
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
|
||||
CacheRadios();
|
||||
CacheEmotes();
|
||||
}
|
||||
|
||||
protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj)
|
||||
{
|
||||
if (obj.WasModified<RadioChannelPrototype>())
|
||||
CacheRadios();
|
||||
|
||||
if (obj.WasModified<EmotePrototype>())
|
||||
CacheEmotes();
|
||||
}
|
||||
|
||||
private void CacheRadios()
|
||||
@@ -127,7 +146,7 @@ public abstract class SharedChatSystem : EntitySystem
|
||||
/// <param name="channel">The channel that was requested, if any</param>
|
||||
/// <param name="quiet">Whether or not to generate an informative pop-up message.</param>
|
||||
/// <returns></returns>
|
||||
public bool TryProccessRadioMessage(
|
||||
public bool TryProcessRadioMessage(
|
||||
EntityUid source,
|
||||
string input,
|
||||
out string output,
|
||||
@@ -293,4 +312,177 @@ public abstract class SharedChatSystem : EntitySystem
|
||||
tagStart += tag.Length + 2;
|
||||
return rawmsg.Substring(tagStart, tagEnd - tagStart);
|
||||
}
|
||||
|
||||
protected virtual void SendEntityEmote(
|
||||
EntityUid source,
|
||||
string action,
|
||||
ChatTransmitRange range,
|
||||
string? nameOverride,
|
||||
bool hideLog = false,
|
||||
bool checkEmote = true,
|
||||
bool ignoreActionBlocker = false,
|
||||
NetUserId? author = null
|
||||
)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Sends an in-character chat message to relevant clients.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity that is speaking.</param>
|
||||
/// <param name="message">The message being spoken or emoted.</param>
|
||||
/// <param name="desiredType">The chat type.</param>
|
||||
/// <param name="hideChat">Whether or not this message should appear in the chat window.</param>
|
||||
/// <param name="hideLog">Whether or not this message should appear in the adminlog window.</param>
|
||||
/// <param name="shell"></param>
|
||||
/// <param name="player">The player doing the speaking.</param>
|
||||
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||
/// <param name="checkRadioPrefix">Whether or not <paramref name="message"/> should be parsed with consideration of radio channel prefix text at start the start.</param>
|
||||
/// <param name="ignoreActionBlocker">If set to true, action blocker will not be considered for whether an entity can send this message.</param>
|
||||
public virtual void TrySendInGameICMessage(
|
||||
EntityUid source,
|
||||
string message,
|
||||
InGameICChatType desiredType,
|
||||
bool hideChat,
|
||||
bool hideLog = false,
|
||||
IConsoleShell? shell = null,
|
||||
ICommonSession? player = null,
|
||||
string? nameOverride = null,
|
||||
bool checkRadioPrefix = true,
|
||||
bool ignoreActionBlocker = false)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Sends an in-character chat message to relevant clients.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity that is speaking.</param>
|
||||
/// <param name="message">The message being spoken or emoted.</param>
|
||||
/// <param name="desiredType">The chat type.</param>
|
||||
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
|
||||
/// <param name="hideLog">Disables the admin log for this message if true. Used for entities that are not players, like vendors, cloning, etc.</param>
|
||||
/// <param name="shell"></param>
|
||||
/// <param name="player">The player doing the speaking.</param>
|
||||
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||
/// <param name="ignoreActionBlocker">If set to true, action blocker will not be considered for whether an entity can send this message.</param>
|
||||
public virtual void TrySendInGameICMessage(
|
||||
EntityUid source,
|
||||
string message,
|
||||
InGameICChatType desiredType,
|
||||
ChatTransmitRange range,
|
||||
bool hideLog = false,
|
||||
IConsoleShell? shell = null,
|
||||
ICommonSession? player = null,
|
||||
string? nameOverride = null,
|
||||
bool checkRadioPrefix = true,
|
||||
bool ignoreActionBlocker = false
|
||||
)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Sends an out-of-character chat message to relevant clients.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity that is speaking.</param>
|
||||
/// <param name="message">The message being spoken or emoted.</param>
|
||||
/// <param name="type">The chat type.</param>
|
||||
/// <param name="hideChat">Whether or not to show the message in the chat window.</param>
|
||||
/// <param name="shell"></param>
|
||||
/// <param name="player">The player doing the speaking.</param>
|
||||
public virtual void TrySendInGameOOCMessage(
|
||||
EntityUid source,
|
||||
string message,
|
||||
InGameOOCChatType type,
|
||||
bool hideChat,
|
||||
IConsoleShell? shell = null,
|
||||
ICommonSession? player = null
|
||||
)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement to all.
|
||||
/// </summary>
|
||||
/// <param name="message">The contents of the message.</param>
|
||||
/// <param name="sender">The sender (Communications Console in Communications Console Announcement).</param>
|
||||
/// <param name="playSound">Play the announcement sound.</param>
|
||||
/// <param name="announcementSound">Sound to play.</param>
|
||||
/// <param name="colorOverride">Optional color for the announcement message.</param>
|
||||
public virtual void DispatchGlobalAnnouncement(
|
||||
string message,
|
||||
string? sender = null,
|
||||
bool playSound = true,
|
||||
SoundSpecifier? announcementSound = null,
|
||||
Color? colorOverride = null
|
||||
)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement to players selected by filter.
|
||||
/// </summary>
|
||||
/// <param name="filter">Filter to select players who will recieve the announcement.</param>
|
||||
/// <param name="message">The contents of the message.</param>
|
||||
/// <param name="source">The entity making the announcement (used to determine the station).</param>
|
||||
/// <param name="sender">The sender (Communications Console in Communications Console Announcement).</param>
|
||||
/// <param name="playSound">Play the announcement sound.</param>
|
||||
/// <param name="announcementSound">Sound to play.</param>
|
||||
/// <param name="colorOverride">Optional color for the announcement message.</param>
|
||||
public virtual void DispatchFilteredAnnouncement(
|
||||
Filter filter,
|
||||
string message,
|
||||
EntityUid? source = null,
|
||||
string? sender = null,
|
||||
bool playSound = true,
|
||||
SoundSpecifier? announcementSound = null,
|
||||
Color? colorOverride = null)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement on a specific station.
|
||||
/// </summary>
|
||||
/// <param name="source">The entity making the announcement (used to determine the station).</param>
|
||||
/// <param name="message">The contents of the message.</param>
|
||||
/// <param name="sender">The sender (Communications Console in Communications Console Announcement).</param>
|
||||
/// <param name="playDefaultSound">Play the announcement sound.</param>
|
||||
/// <param name="announcementSound">Sound to play.</param>
|
||||
/// <param name="colorOverride">Optional color for the announcement message.</param>
|
||||
public virtual void DispatchStationAnnouncement(
|
||||
EntityUid source,
|
||||
string message,
|
||||
string? sender = null,
|
||||
bool playDefaultSound = true,
|
||||
SoundSpecifier? announcementSound = null,
|
||||
Color? colorOverride = null)
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls transmission of chat.
|
||||
/// </summary>
|
||||
public enum ChatTransmitRange : byte
|
||||
{
|
||||
/// Acts normal, ghosts can hear across the map, etc.
|
||||
Normal,
|
||||
/// Normal but ghosts are still range-limited.
|
||||
GhostRangeLimit,
|
||||
/// Hidden from the chat window.
|
||||
HideChat,
|
||||
/// Ghosts can't hear or see it at all. Regular players can if in-range.
|
||||
NoGhosts
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public enum InGameICChatType : byte
|
||||
{
|
||||
Speak,
|
||||
Emote,
|
||||
Whisper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC.
|
||||
/// </summary>
|
||||
public enum InGameOOCChatType : byte
|
||||
{
|
||||
Looc,
|
||||
Dead
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Shared.Chat;
|
||||
|
||||
@@ -40,7 +42,7 @@ public sealed class SharedSuicideSystem : EntitySystem
|
||||
appliedDamageSpecifier.DamageDict[key] = Math.Ceiling((double) (value * lethalAmountOfDamage / totalDamage));
|
||||
}
|
||||
|
||||
_damageableSystem.TryChangeDamage(target, appliedDamageSpecifier, true, origin: target);
|
||||
_damageableSystem.ChangeDamage(target.AsNullable(), appliedDamageSpecifier, true, origin: target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,6 +66,6 @@ public sealed class SharedSuicideSystem : EntitySystem
|
||||
}
|
||||
|
||||
var damage = new DamageSpecifier(damagePrototype, lethalAmountOfDamage);
|
||||
_damageableSystem.TryChangeDamage(target, damage, true, origin: target);
|
||||
_damageableSystem.ChangeDamage(target.AsNullable(), damage, true, origin: target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ using Robust.Shared.GameStates;
|
||||
namespace Content.Shared.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Denotes the solution that can be easily removed through any reagent container.
|
||||
/// Think pouring this or draining from a water tank.
|
||||
/// Denotes a specific solution contained within this entity that can can be
|
||||
/// easily "drained". This means things with taps/spigots, or easily poured
|
||||
/// items.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class DrainableSolutionComponent : Component
|
||||
@@ -12,6 +13,12 @@ public sealed partial class DrainableSolutionComponent : Component
|
||||
/// <summary>
|
||||
/// Solution name that can be drained.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public string Solution = "default";
|
||||
|
||||
/// <summary>
|
||||
/// The drain doafter time required to transfer reagents from the solution.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan DrainTime = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ using Robust.Shared.GameStates;
|
||||
namespace Content.Shared.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Denotes the solution that can be easily dumped into (completely removed from the dumping container into this one)
|
||||
/// Think pouring a container fully into this.
|
||||
/// Denotes that there is a solution contained in this entity that can be
|
||||
/// easily dumped into (that is, completely removed from the dumping container
|
||||
/// into this one). Think pouring a container fully into this. The action for this is represented via drag & drop.
|
||||
///
|
||||
/// To represent it being possible to controllably pour volumes into the entity, see <see cref="RefillableSolutionComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class DumpableSolutionComponent : Component
|
||||
@@ -12,12 +15,13 @@ public sealed partial class DumpableSolutionComponent : Component
|
||||
/// <summary>
|
||||
/// Solution name that can be dumped into.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public string Solution = "default";
|
||||
|
||||
/// <summary>
|
||||
/// Whether the solution can be dumped into infinitely.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
/// <remarks>Note that this is what makes the ChemMaster's buffer a stasis buffer as well!</remarks>
|
||||
[DataField]
|
||||
public bool Unlimited = false;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ public sealed partial class HyposprayComponent : Component
|
||||
[DataField]
|
||||
public FixedPoint2 TransferAmount = FixedPoint2.New(5);
|
||||
|
||||
/// <summary>
|
||||
/// The delay to draw reagents using the hypospray.
|
||||
/// If set, <see cref="RefillableSolutionComponent"/> RefillTime should probably have the same value.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float DrawTime = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Sound that will be played when injecting.
|
||||
/// </summary>
|
||||
|
||||
@@ -4,9 +4,11 @@ using Robust.Shared.GameStates;
|
||||
namespace Content.Shared.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Reagents that can be added easily. For example like
|
||||
/// pouring something into another beaker, glass, or into the gas
|
||||
/// tank of a car.
|
||||
/// Denotes that the entity has a solution contained which can be easily added
|
||||
/// to in controlled volumes. This should go on things that are meant to be refilled, including
|
||||
/// pouring things into a beaker. The action for this is represented via clicking.
|
||||
///
|
||||
/// To represent it being possible to just dump entire volumes at once into an entity, see <see cref="DumpableSolutionComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class RefillableSolutionComponent : Component
|
||||
@@ -14,12 +16,18 @@ public sealed partial class RefillableSolutionComponent : Component
|
||||
/// <summary>
|
||||
/// Solution name that can added to easily.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public string Solution = "default";
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount that can be transferred to the solution at once
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public FixedPoint2? MaxRefill = null;
|
||||
|
||||
/// <summary>
|
||||
/// The refill doafter time required to transfer reagents into the solution.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan RefillTime = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ namespace Content.Shared.Chemistry.Components
|
||||
/// systems use this.
|
||||
/// </remarks>
|
||||
[DataField("maxVol")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public FixedPoint2 MaxVolume { get; set; } = FixedPoint2.Zero;
|
||||
|
||||
public float FillFraction => MaxVolume == 0 ? 1 : Volume.Float() / MaxVolume.Float();
|
||||
@@ -45,8 +44,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
/// <summary>
|
||||
/// If reactions will be checked for when adding reagents to the container.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("canReact")]
|
||||
[DataField]
|
||||
public bool CanReact { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -58,8 +56,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
/// <summary>
|
||||
/// The temperature of the reagents in the solution.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("temperature")]
|
||||
[DataField]
|
||||
public float Temperature { get; set; } = 293.15f;
|
||||
|
||||
/// <summary>
|
||||
@@ -100,7 +97,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
_heatCapacity = 0;
|
||||
foreach (var (reagent, quantity) in Contents)
|
||||
{
|
||||
_heatCapacity += (float) quantity *
|
||||
_heatCapacity += (float)quantity *
|
||||
protoMan.Index<ReagentPrototype>(reagent.Prototype).SpecificHeat;
|
||||
}
|
||||
|
||||
@@ -148,7 +145,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
/// </summary>
|
||||
/// <param name="prototype">The prototype ID of the reagent to add.</param>
|
||||
/// <param name="quantity">The quantity in milli-units.</param>
|
||||
public Solution(string prototype, FixedPoint2 quantity, List<ReagentData>? data = null) : this()
|
||||
public Solution([ForbidLiteral] string prototype, FixedPoint2 quantity, List<ReagentData>? data = null) : this()
|
||||
{
|
||||
AddReagent(new ReagentId(prototype, data), quantity);
|
||||
}
|
||||
@@ -190,7 +187,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
public void ValidateSolution()
|
||||
{
|
||||
// sandbox forbids: [Conditional("DEBUG")]
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
// Correct volume
|
||||
DebugTools.Assert(Contents.Select(x => x.Quantity).Sum() == Volume);
|
||||
|
||||
@@ -208,7 +205,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
UpdateHeatCapacity(null);
|
||||
DebugTools.Assert(MathHelper.CloseTo(_heatCapacity, cur, tolerance: 0.01));
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
@@ -223,7 +220,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
MaxVolume = Volume;
|
||||
}
|
||||
|
||||
public bool ContainsPrototype(string prototype)
|
||||
public bool ContainsPrototype([ForbidLiteral] string prototype)
|
||||
{
|
||||
foreach (var (reagent, _) in Contents)
|
||||
{
|
||||
@@ -245,7 +242,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ContainsReagent(string reagentId, List<ReagentData>? data)
|
||||
public bool ContainsReagent([ForbidLiteral] string reagentId, List<ReagentData>? data)
|
||||
=> ContainsReagent(new(reagentId, data));
|
||||
|
||||
public bool TryGetReagent(ReagentId id, out ReagentQuantity quantity)
|
||||
@@ -352,7 +349,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
/// </summary>
|
||||
/// <param name="prototype">The prototype ID of the reagent to add.</param>
|
||||
/// <param name="quantity">The quantity in milli-units.</param>
|
||||
public void AddReagent(string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true)
|
||||
public void AddReagent([ForbidLiteral] string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true)
|
||||
=> AddReagent(new ReagentId(prototype, null), quantity, dirtyHeatCap);
|
||||
|
||||
/// <summary>
|
||||
@@ -673,6 +670,12 @@ namespace Content.Shared.Chemistry.Components
|
||||
return sol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a solution into two by moving reagents from the given solution into a new one.
|
||||
/// This modifies the original solution.
|
||||
/// </summary>
|
||||
/// <param name="toTake">The quantity of this solution to remove.</param>
|
||||
/// <returns>A new solution containing the removed reagents.</returns>
|
||||
public Solution SplitSolution(FixedPoint2 toTake)
|
||||
{
|
||||
if (toTake <= FixedPoint2.Zero)
|
||||
@@ -690,7 +693,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
var origVol = Volume;
|
||||
var effVol = Volume.Value;
|
||||
newSolution = new Solution(Contents.Count) { Temperature = Temperature };
|
||||
var remaining = (long) toTake.Value;
|
||||
var remaining = (long)toTake.Value;
|
||||
|
||||
for (var i = Contents.Count - 1; i >= 0; i--) // iterate backwards because of remove swap.
|
||||
{
|
||||
@@ -706,7 +709,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
continue;
|
||||
}
|
||||
|
||||
var splitQuantity = FixedPoint2.FromCents((int) split);
|
||||
var splitQuantity = FixedPoint2.FromCents((int)split);
|
||||
var newQuantity = quantity - splitQuantity;
|
||||
|
||||
DebugTools.Assert(newQuantity >= 0);
|
||||
@@ -753,7 +756,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
|
||||
var effVol = Volume.Value;
|
||||
Volume -= toTake;
|
||||
var remaining = (long) toTake.Value;
|
||||
var remaining = (long)toTake.Value;
|
||||
for (var i = Contents.Count - 1; i >= 0; i--)// iterate backwards because of remove swap.
|
||||
{
|
||||
var (reagent, quantity) = Contents[i];
|
||||
@@ -768,7 +771,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
continue;
|
||||
}
|
||||
|
||||
var splitQuantity = FixedPoint2.FromCents((int) split);
|
||||
var splitQuantity = FixedPoint2.FromCents((int)split);
|
||||
var newQuantity = quantity - splitQuantity;
|
||||
|
||||
if (newQuantity > FixedPoint2.Zero)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.Hypospray.Events;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Forensics;
|
||||
using Content.Shared.IdentityManagement;
|
||||
@@ -16,6 +16,7 @@ using Content.Shared.Timing;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
@@ -27,6 +28,7 @@ public sealed class HypospraySystem : EntitySystem
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
|
||||
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -36,6 +38,7 @@ public sealed class HypospraySystem : EntitySystem
|
||||
SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
|
||||
SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
|
||||
SubscribeLocalEvent<HyposprayComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleModeVerb);
|
||||
SubscribeLocalEvent<HyposprayComponent, HyposprayDrawDoAfterEvent>(OnDrawDoAfter);
|
||||
}
|
||||
|
||||
#region Ref events
|
||||
@@ -63,6 +66,20 @@ public sealed class HypospraySystem : EntitySystem
|
||||
TryDoInject(entity, args.HitEntities[0], args.User);
|
||||
}
|
||||
|
||||
private void OnDrawDoAfter(Entity<HyposprayComponent> entity, ref HyposprayDrawDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (entity.Comp.CanContainerDraw
|
||||
&& args.Target.HasValue
|
||||
&& !EligibleEntity(args.Target.Value, entity)
|
||||
&& _solutionContainers.TryGetDrawableSolution(args.Target.Value, out var drawableSolution, out _))
|
||||
{
|
||||
TryDraw(entity, args.Target.Value, drawableSolution.Value, args.User);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Draw/Inject
|
||||
@@ -73,7 +90,7 @@ public sealed class HypospraySystem : EntitySystem
|
||||
&& !EligibleEntity(target, entity)
|
||||
&& _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
|
||||
{
|
||||
return TryDraw(entity, target, drawableSolution.Value, user);
|
||||
return TryStartDraw(entity, target, drawableSolution.Value, user);
|
||||
}
|
||||
|
||||
return TryDoInject(entity, target, user);
|
||||
@@ -186,17 +203,37 @@ public sealed class HypospraySystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
|
||||
public bool TryStartDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
|
||||
{
|
||||
if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln,
|
||||
out var solution) || solution.AvailableVolume == 0)
|
||||
if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln))
|
||||
return false;
|
||||
|
||||
if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out _))
|
||||
return false;
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, user, entity.Comp.DrawTime, new HyposprayDrawDoAfterEvent(), entity, target)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
Hidden = true,
|
||||
};
|
||||
|
||||
return _doAfter.TryStartDoAfter(doAfterArgs, out _);
|
||||
}
|
||||
|
||||
private bool TryGetDrawAmount(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user, Entity<SolutionComponent> solutionEntity, [NotNullWhen(true)] out FixedPoint2? amount)
|
||||
{
|
||||
amount = null;
|
||||
|
||||
if (solutionEntity.Comp.Solution.AvailableVolume == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
|
||||
var realTransferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, targetSolution.Comp.Solution.Volume,
|
||||
solution.AvailableVolume);
|
||||
solutionEntity.Comp.Solution.AvailableVolume);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
@@ -207,7 +244,19 @@ public sealed class HypospraySystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
var removedSolution = _solutionContainers.Draw(target, targetSolution, realTransferAmount);
|
||||
amount = realTransferAmount;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
|
||||
{
|
||||
if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln))
|
||||
return false;
|
||||
|
||||
if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out var amount))
|
||||
return false;
|
||||
|
||||
var removedSolution = _solutionContainers.Draw(target, targetSolution, amount.Value);
|
||||
|
||||
if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
|
||||
{
|
||||
@@ -275,3 +324,6 @@ public sealed class HypospraySystem : EntitySystem
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class HyposprayDrawDoAfterEvent : SimpleDoAfterEvent {}
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class ScoopableSolutionSystem : EntitySystem
|
||||
!_solution.TryGetRefillableSolution(beaker, out var target, out _))
|
||||
return false;
|
||||
|
||||
var scooped = _solutionTransfer.Transfer(user, ent, src.Value, beaker, target.Value, srcSolution.Volume);
|
||||
var scooped = _solutionTransfer.Transfer(new SolutionTransferData(user, ent, src.Value, beaker, target.Value, srcSolution.Volume));
|
||||
if (scooped == 0)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -588,7 +588,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
/// Adds a solution to the container, if it can fully fit.
|
||||
/// </summary>
|
||||
/// <param name="targetUid">entity holding targetSolution</param>
|
||||
/// <param name="targetSolution">entity holding targetSolution</param>
|
||||
/// <param name="targetSolution">entity holding targetSolution</param>
|
||||
/// <param name="toAdd">solution being added</param>
|
||||
/// <returns>If the solution could be added.</returns>
|
||||
public bool TryAddSolution(Entity<SolutionComponent> soln, Solution toAdd)
|
||||
@@ -606,40 +606,44 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds as much of a solution to a container as can fit.
|
||||
/// Adds as much of a solution to a container as can fit and updates the container.
|
||||
/// </summary>
|
||||
/// <param name="targetUid">The entity containing <paramref cref="targetSolution"/></param>
|
||||
/// <param name="targetSolution">The solution being added to.</param>
|
||||
/// <param name="toAdd">The solution being added to <paramref cref="targetSolution"/></param>
|
||||
/// <param name="toAdd">The solution being added to <paramref cref="targetSolution"/>. This solution is not modified.</param>
|
||||
/// <returns>The quantity of the solution actually added.</returns>
|
||||
public FixedPoint2 AddSolution(Entity<SolutionComponent> soln, Solution toAdd)
|
||||
{
|
||||
var (uid, comp) = soln;
|
||||
var solution = comp.Solution;
|
||||
var solution = soln.Comp.Solution;
|
||||
|
||||
if (toAdd.Volume == FixedPoint2.Zero)
|
||||
return FixedPoint2.Zero;
|
||||
|
||||
var quantity = FixedPoint2.Max(FixedPoint2.Zero, FixedPoint2.Min(toAdd.Volume, solution.AvailableVolume));
|
||||
if (quantity < toAdd.Volume)
|
||||
TryTransferSolution(soln, toAdd, quantity);
|
||||
{
|
||||
// TODO: This should be made into a function that directly transfers reagents.
|
||||
// Currently this is quite inefficient.
|
||||
solution.AddSolution(toAdd.Clone().SplitSolution(quantity), PrototypeManager);
|
||||
}
|
||||
else
|
||||
ForceAddSolution(soln, toAdd);
|
||||
solution.AddSolution(toAdd, PrototypeManager);
|
||||
|
||||
UpdateChemicals(soln);
|
||||
return quantity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a solution to a container and updates the container.
|
||||
/// This can exceed the maximum volume of the solution added to.
|
||||
/// </summary>
|
||||
/// <param name="targetUid">The entity containing <paramref cref="targetSolution"/></param>
|
||||
/// <param name="targetSolution">The solution being added to.</param>
|
||||
/// <param name="toAdd">The solution being added to <paramref cref="targetSolution"/></param>
|
||||
/// <param name="toAdd">The solution being added to <paramref cref="targetSolution"/>. This solution is not modified.</param>
|
||||
/// <returns>Whether any reagents were added to the solution.</returns>
|
||||
public bool ForceAddSolution(Entity<SolutionComponent> soln, Solution toAdd)
|
||||
{
|
||||
var (uid, comp) = soln;
|
||||
var solution = comp.Solution;
|
||||
var solution = soln.Comp.Solution;
|
||||
|
||||
if (toAdd.Volume == FixedPoint2.Zero)
|
||||
return false;
|
||||
@@ -707,6 +711,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
}
|
||||
|
||||
// Thermal energy and temperature management.
|
||||
// TODO: ENERGY CONSERVATION!!! Nuke this once we have HeatContainers and use methods which properly conserve energy and model heat transfer correctly!
|
||||
|
||||
#region Thermal Energy and Temperature
|
||||
|
||||
@@ -763,6 +768,26 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
UpdateChemicals(soln);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="AddThermalEnergy"/> but clamps the value between two temperature values.
|
||||
/// </summary>
|
||||
/// <param name="soln">Solution we're adjusting the energy of</param>
|
||||
/// <param name="thermalEnergy">Thermal energy we're adding or removing</param>
|
||||
/// <param name="min">Min desired temperature</param>
|
||||
/// <param name="max">Max desired temperature</param>
|
||||
public void AddThermalEnergyClamped(Entity<SolutionComponent> soln, float thermalEnergy, float min, float max)
|
||||
{
|
||||
var solution = soln.Comp.Solution;
|
||||
|
||||
if (thermalEnergy == 0.0f)
|
||||
return;
|
||||
|
||||
var heatCap = solution.GetHeatCapacity(PrototypeManager);
|
||||
var deltaT = thermalEnergy / heatCap;
|
||||
solution.Temperature = Math.Clamp(solution.Temperature + deltaT, min, max);
|
||||
UpdateChemicals(soln);
|
||||
}
|
||||
|
||||
#endregion Thermal Energy and Temperature
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Allows an entity to transfer solutions with a customizable amount per click.
|
||||
/// Also provides <see cref="Transfer"/> API for other systems.
|
||||
/// Allows an entity to transfer solutions with a customizable amount -per click-.
|
||||
/// Also provides <see cref="Transfer"/>, <see cref="RefillTransfer"/> and <see cref="DrainTransfer"/> API for other systems.
|
||||
/// </summary>
|
||||
public sealed class SolutionTransferSystem : EntitySystem
|
||||
{
|
||||
@@ -21,6 +21,10 @@ public sealed class SolutionTransferSystem : EntitySystem
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
|
||||
private EntityQuery<RefillableSolutionComponent> _refillableQuery;
|
||||
private EntityQuery<DrainableSolutionComponent> _drainableQuery;
|
||||
|
||||
/// <summary>
|
||||
/// Default transfer amounts for the set-transfer verb.
|
||||
@@ -32,28 +36,18 @@ public sealed class SolutionTransferSystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SolutionTransferComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
|
||||
SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<SolutionTransferComponent, TransferAmountSetValueMessage>(OnTransferAmountSetValueMessage);
|
||||
}
|
||||
SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<SolutionTransferComponent, SolutionDrainTransferDoAfterEvent>(OnSolutionDrainTransferDoAfter);
|
||||
SubscribeLocalEvent<SolutionTransferComponent, SolutionRefillTransferDoAfterEvent>(OnSolutionFillTransferDoAfter);
|
||||
|
||||
private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> ent, ref TransferAmountSetValueMessage message)
|
||||
{
|
||||
var (uid, comp) = ent;
|
||||
|
||||
var newTransferAmount = FixedPoint2.Clamp(message.Value, comp.MinimumTransferAmount, comp.MaximumTransferAmount);
|
||||
comp.TransferAmount = newTransferAmount;
|
||||
|
||||
if (message.Actor is { Valid: true } user)
|
||||
_popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), uid, user);
|
||||
|
||||
Dirty(uid, comp);
|
||||
_refillableQuery = GetEntityQuery<RefillableSolutionComponent>();
|
||||
_drainableQuery = GetEntityQuery<DrainableSolutionComponent>();
|
||||
}
|
||||
|
||||
private void AddSetTransferVerbs(Entity<SolutionTransferComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
var (uid, comp) = ent;
|
||||
|
||||
if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null)
|
||||
if (!args.CanAccess || !args.CanInteract || !ent.Comp.CanChangeTransferAmount || args.Hands == null)
|
||||
return;
|
||||
|
||||
// Custom transfer verb
|
||||
@@ -66,7 +60,7 @@ public sealed class SolutionTransferSystem : EntitySystem
|
||||
// TODO: remove server check when bui prediction is a thing
|
||||
Act = () =>
|
||||
{
|
||||
_ui.OpenUi(uid, TransferAmountUiKey.Key, @event.User);
|
||||
_ui.OpenUi(ent.Owner, TransferAmountUiKey.Key, @event.User);
|
||||
},
|
||||
Priority = 1
|
||||
});
|
||||
@@ -76,7 +70,7 @@ public sealed class SolutionTransferSystem : EntitySystem
|
||||
var user = args.User;
|
||||
foreach (var amount in DefaultTransferAmounts)
|
||||
{
|
||||
if (amount < comp.MinimumTransferAmount || amount > comp.MaximumTransferAmount)
|
||||
if (amount < ent.Comp.MinimumTransferAmount || amount > ent.Comp.MaximumTransferAmount)
|
||||
continue;
|
||||
|
||||
AlternativeVerb verb = new();
|
||||
@@ -84,11 +78,11 @@ public sealed class SolutionTransferSystem : EntitySystem
|
||||
verb.Category = VerbCategory.SetTransferAmount;
|
||||
verb.Act = () =>
|
||||
{
|
||||
comp.TransferAmount = amount;
|
||||
ent.Comp.TransferAmount = amount;
|
||||
|
||||
_popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user);
|
||||
_popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), ent.Owner, user);
|
||||
|
||||
Dirty(uid, comp);
|
||||
Dirty(ent.Owner, ent.Comp);
|
||||
};
|
||||
|
||||
// we want to sort by size, not alphabetically by the verb text.
|
||||
@@ -99,117 +93,301 @@ public sealed class SolutionTransferSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> ent, ref TransferAmountSetValueMessage message)
|
||||
{
|
||||
var newTransferAmount = FixedPoint2.Clamp(message.Value, ent.Comp.MinimumTransferAmount, ent.Comp.MaximumTransferAmount);
|
||||
ent.Comp.TransferAmount = newTransferAmount;
|
||||
|
||||
if (message.Actor is { Valid: true } user)
|
||||
_popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), ent.Owner, user);
|
||||
|
||||
Dirty(ent.Owner, ent.Comp);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(Entity<SolutionTransferComponent> ent, ref AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach || args.Target is not {} target)
|
||||
return;
|
||||
|
||||
var (uid, comp) = ent;
|
||||
// We have two cases for interaction:
|
||||
// Held Drainable --> Target Refillable
|
||||
// Held Refillable <-- Target Drainable
|
||||
|
||||
//Special case for reagent tanks, because normally clicking another container will give solution, not take it.
|
||||
if (comp.CanReceive
|
||||
&& !HasComp<RefillableSolutionComponent>(target) // target must not be refillable (e.g. Reagent Tanks)
|
||||
&& _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable
|
||||
&& TryComp<RefillableSolutionComponent>(uid, out var refill)
|
||||
&& _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill))
|
||||
// In the case where the target has both Refillable and Drainable, Held --> Target takes priority.
|
||||
|
||||
if (ent.Comp.CanSend
|
||||
&& _drainableQuery.TryComp(ent.Owner, out var heldDrainable)
|
||||
&& _refillableQuery.TryComp(target, out var targetRefillable)
|
||||
&& TryGetTransferrableSolutions((ent.Owner, heldDrainable),
|
||||
(target, targetRefillable),
|
||||
out var ownerSoln,
|
||||
out var targetSoln,
|
||||
out _))
|
||||
{
|
||||
var transferAmount = comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank.
|
||||
args.Handled = true; //If we reach this point, the interaction counts as handled.
|
||||
|
||||
// if the receiver has a smaller transfer limit, use that instead
|
||||
if (refill?.MaxRefill is {} maxRefill)
|
||||
var transferAmount = ent.Comp.TransferAmount;
|
||||
if (targetRefillable.MaxRefill is {} maxRefill)
|
||||
transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
|
||||
|
||||
var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount);
|
||||
args.Handled = true;
|
||||
if (transferred > 0)
|
||||
{
|
||||
var toTheBrim = ownerRefill.AvailableVolume == 0;
|
||||
var msg = toTheBrim
|
||||
? "comp-solution-transfer-fill-fully"
|
||||
: "comp-solution-transfer-fill-normal";
|
||||
var transferData = new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, target, targetSoln.Value, transferAmount);
|
||||
var transferTime = targetRefillable.RefillTime + heldDrainable.DrainTime;
|
||||
|
||||
_popup.PopupClient(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User);
|
||||
return;
|
||||
if (transferTime > TimeSpan.Zero)
|
||||
{
|
||||
if (!CanTransfer(transferData))
|
||||
return;
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionDrainTransferDoAfterEvent(transferAmount), ent.Owner, target)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
Hidden = true,
|
||||
};
|
||||
_doAfter.TryStartDoAfter(doAfterArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrainTransfer(transferData);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if target is refillable, and owner is drainable
|
||||
if (comp.CanSend
|
||||
&& TryComp<RefillableSolutionComponent>(target, out var targetRefill)
|
||||
&& _solution.TryGetRefillableSolution((target, targetRefill, null), out targetSoln, out _)
|
||||
&& _solution.TryGetDrainableSolution(uid, out ownerSoln, out _))
|
||||
if (ent.Comp.CanReceive
|
||||
&& _refillableQuery.TryComp(ent.Owner, out var heldRefillable)
|
||||
&& _drainableQuery.TryComp(target, out var targetDrainable)
|
||||
&& TryGetTransferrableSolutions((target, targetDrainable),
|
||||
(ent.Owner, heldRefillable),
|
||||
out targetSoln,
|
||||
out ownerSoln,
|
||||
out var solution))
|
||||
{
|
||||
var transferAmount = comp.TransferAmount;
|
||||
args.Handled = true; //If we reach this point, the interaction counts as handled.
|
||||
|
||||
if (targetRefill?.MaxRefill is {} maxRefill)
|
||||
var transferAmount = ent.Comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target drainable.
|
||||
if (heldRefillable.MaxRefill is {} maxRefill) // if the receiver has a smaller transfer limit, use that instead
|
||||
transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
|
||||
|
||||
var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount);
|
||||
args.Handled = true;
|
||||
if (transferred > 0)
|
||||
var transferData = new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, transferAmount);
|
||||
var transferTime = heldRefillable.RefillTime + targetDrainable.DrainTime;
|
||||
|
||||
if (transferTime > TimeSpan.Zero)
|
||||
{
|
||||
var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target));
|
||||
_popup.PopupClient(message, uid, args.User);
|
||||
if (!CanTransfer(transferData))
|
||||
return;
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionRefillTransferDoAfterEvent(transferAmount), ent.Owner, target)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
Hidden = true,
|
||||
};
|
||||
_doAfter.TryStartDoAfter(doAfterArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
RefillTransfer(transferData, solution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSolutionDrainTransferDoAfter(Entity<SolutionTransferComponent> ent, ref SolutionDrainTransferDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Target is not { } target)
|
||||
return;
|
||||
|
||||
// Have to check again, in case something has changed.
|
||||
if (CanSend(ent, target, out var ownerSoln, out var targetSoln))
|
||||
{
|
||||
DrainTransfer(new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, args.Target.Value, targetSoln.Value, args.Amount));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSolutionFillTransferDoAfter(Entity<SolutionTransferComponent> ent, ref SolutionRefillTransferDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled || args.Target is not { } target)
|
||||
return;
|
||||
|
||||
// Have to check again, in case something has changed.
|
||||
if (!CanRecieve(ent, target, out var ownerSoln, out var targetSoln, out var solution))
|
||||
return;
|
||||
|
||||
RefillTransfer(new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, args.Amount), solution);
|
||||
}
|
||||
|
||||
private bool CanSend(Entity<SolutionTransferComponent, DrainableSolutionComponent?> ent,
|
||||
Entity<RefillableSolutionComponent?> target,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? drainable,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? refillable)
|
||||
{
|
||||
drainable = null;
|
||||
refillable = null;
|
||||
|
||||
return ent.Comp1.CanReceive && TryGetTransferrableSolutions(ent.Owner, target, out drainable, out refillable, out _);
|
||||
}
|
||||
|
||||
private bool CanRecieve(Entity<SolutionTransferComponent> ent,
|
||||
EntityUid source,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? drainable,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? refillable,
|
||||
[NotNullWhen(true)] out Solution? solution)
|
||||
{
|
||||
drainable = null;
|
||||
refillable = null;
|
||||
solution = null;
|
||||
|
||||
return ent.Comp.CanReceive && TryGetTransferrableSolutions(source, ent.Owner, out drainable, out refillable, out solution);
|
||||
}
|
||||
|
||||
private bool TryGetTransferrableSolutions(Entity<DrainableSolutionComponent?> source,
|
||||
Entity<RefillableSolutionComponent?> target,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? drainable,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? refillable,
|
||||
[NotNullWhen(true)] out Solution? solution)
|
||||
{
|
||||
drainable = null;
|
||||
refillable = null;
|
||||
solution = null;
|
||||
|
||||
if (!_drainableQuery.Resolve(source, ref source.Comp) || !_refillableQuery.Resolve(target, ref target.Comp))
|
||||
return false;
|
||||
|
||||
if (!_solution.TryGetDrainableSolution(source, out drainable, out _))
|
||||
return false;
|
||||
|
||||
if (!_solution.TryGetRefillableSolution(target, out refillable, out solution))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer from a solution to another, allowing either entity to cancel it and show a popup.
|
||||
/// Attempt to drain a solution into another, such as pouring a bottle into a glass.
|
||||
/// Includes a pop-up if the transfer failed or succeeded
|
||||
/// </summary>
|
||||
/// <param name="data">The transfer data making up the transfer.</param>
|
||||
/// <returns>The actual amount transferred.</returns>
|
||||
private void DrainTransfer(SolutionTransferData data)
|
||||
{
|
||||
var transferred = Transfer(data);
|
||||
if (transferred <= 0)
|
||||
return;
|
||||
|
||||
var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", data.TargetEntity));
|
||||
_popup.PopupClient(message, data.SourceEntity, data.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to fill a solution from another container, such as tapping from a water tank.
|
||||
/// Includes a pop-up if the transfer failed or succeeded.
|
||||
/// </summary>
|
||||
/// <param name="data">The transfer data making up the transfer.</param>
|
||||
/// <param name="targetSolution">The target solution,included for LoC pop-up purposes.</param>
|
||||
/// <returns>The actual amount transferred.</returns>
|
||||
private void RefillTransfer(SolutionTransferData data, Solution targetSolution)
|
||||
{
|
||||
var transferred = Transfer(data);
|
||||
if (transferred <= 0)
|
||||
return;
|
||||
|
||||
var toTheBrim = targetSolution.AvailableVolume == 0;
|
||||
var msg = toTheBrim
|
||||
? "comp-solution-transfer-fill-fully"
|
||||
: "comp-solution-transfer-fill-normal";
|
||||
|
||||
_popup.PopupClient(Loc.GetString(msg, ("owner", data.SourceEntity), ("amount", transferred), ("target", data.TargetEntity)), data.TargetEntity, data.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfer from a solution to another, allowing either entity to cancel.
|
||||
/// Includes a pop-up if the transfer failed.
|
||||
/// </summary>
|
||||
/// <returns>The actual amount transferred.</returns>
|
||||
public FixedPoint2 Transfer(EntityUid user,
|
||||
EntityUid sourceEntity,
|
||||
Entity<SolutionComponent> source,
|
||||
EntityUid targetEntity,
|
||||
Entity<SolutionComponent> target,
|
||||
FixedPoint2 amount)
|
||||
public FixedPoint2 Transfer(SolutionTransferData data)
|
||||
{
|
||||
var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity);
|
||||
var sourceSolution = data.Source.Comp.Solution;
|
||||
var targetSolution = data.Target.Comp.Solution;
|
||||
|
||||
// Check if the source is cancelling the transfer
|
||||
RaiseLocalEvent(sourceEntity, ref transferAttempt);
|
||||
if (transferAttempt.CancelReason is {} reason)
|
||||
{
|
||||
_popup.PopupClient(reason, sourceEntity, user);
|
||||
if (!CanTransfer(data))
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
var sourceSolution = source.Comp.Solution;
|
||||
if (sourceSolution.Volume == 0)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user);
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
var actualAmount = FixedPoint2.Min(data.Amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
|
||||
|
||||
// Check if the target is cancelling the transfer
|
||||
RaiseLocalEvent(targetEntity, ref transferAttempt);
|
||||
if (transferAttempt.CancelReason is {} targetReason)
|
||||
{
|
||||
_popup.PopupClient(targetReason, targetEntity, user);
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
var solution = _solution.SplitSolution(data.Source, actualAmount);
|
||||
_solution.AddSolution(data.Target, solution);
|
||||
|
||||
var targetSolution = target.Comp.Solution;
|
||||
if (targetSolution.AvailableVolume == 0)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user);
|
||||
return FixedPoint2.Zero;
|
||||
}
|
||||
var ev = new SolutionTransferredEvent(data.SourceEntity, data.TargetEntity, data.User, actualAmount);
|
||||
RaiseLocalEvent(data.TargetEntity, ref ev);
|
||||
|
||||
var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
|
||||
|
||||
var solution = _solution.SplitSolution(source, actualAmount);
|
||||
_solution.AddSolution(target, solution);
|
||||
|
||||
var ev = new SolutionTransferredEvent(sourceEntity, targetEntity, user, actualAmount);
|
||||
RaiseLocalEvent(targetEntity, ref ev);
|
||||
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium,
|
||||
$"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");
|
||||
_adminLogger.Add(LogType.Action,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(data.User):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(data.TargetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");
|
||||
|
||||
return actualAmount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the source solution can transfer the amount to the target solution, and display a pop-up if it fails.
|
||||
/// </summary>
|
||||
private bool CanTransfer(SolutionTransferData data)
|
||||
{
|
||||
var transferAttempt = new SolutionTransferAttemptEvent(data.SourceEntity, data.TargetEntity);
|
||||
|
||||
// Check if the source is cancelling the transfer
|
||||
RaiseLocalEvent(data.SourceEntity, ref transferAttempt);
|
||||
if (transferAttempt.CancelReason is {} reason)
|
||||
{
|
||||
_popup.PopupClient(reason, data.SourceEntity, data.User);
|
||||
return false;
|
||||
}
|
||||
|
||||
var sourceSolution = data.Source.Comp.Solution;
|
||||
if (sourceSolution.Volume == 0)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", data.SourceEntity)), data.SourceEntity, data.User);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the target is cancelling the transfer
|
||||
RaiseLocalEvent(data.TargetEntity, ref transferAttempt);
|
||||
if (transferAttempt.CancelReason is {} targetReason)
|
||||
{
|
||||
_popup.PopupClient(targetReason, data.TargetEntity, data.User);
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetSolution = data.Target.Comp.Solution;
|
||||
if (targetSolution.AvailableVolume == 0)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", data.TargetEntity)), data.TargetEntity, data.User);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A collection of data containing relevant entities and values for transferring reagents.
|
||||
/// </summary>
|
||||
/// <param name="user">The user performing the transfer.</param>
|
||||
/// <param name="sourceEntity">The entity holding the solution container which reagents are being moved from.</param>
|
||||
/// <param name="source">The entity holding the solution from which reagents are being moved away from.</param>
|
||||
/// <param name="targetEntity">The entity holding the solution container which reagents are being moved to.</param>
|
||||
/// <param name="target">The entity holding the solution which reagents are being moved to</param>
|
||||
/// <param name="amount">The amount being moved.</param>
|
||||
public struct SolutionTransferData(EntityUid user, EntityUid sourceEntity, Entity<SolutionComponent> source, EntityUid targetEntity, Entity<SolutionComponent> target, FixedPoint2 amount)
|
||||
{
|
||||
public EntityUid User = user;
|
||||
public EntityUid SourceEntity = sourceEntity;
|
||||
public Entity<SolutionComponent> Source = source;
|
||||
public EntityUid TargetEntity = targetEntity;
|
||||
public Entity<SolutionComponent> Target = target;
|
||||
public FixedPoint2 Amount = amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -234,3 +412,35 @@ public record struct SolutionTransferAttemptEvent(EntityUid From, EntityUid To,
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct SolutionTransferredEvent(EntityUid From, EntityUid To, EntityUid User, FixedPoint2 Amount);
|
||||
|
||||
/// <summary>
|
||||
/// Doafter event for solution transfers where the held item is drained into the target. Checks for validity both when initiating and when finishing the event.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class SolutionDrainTransferDoAfterEvent : DoAfterEvent
|
||||
{
|
||||
public FixedPoint2 Amount;
|
||||
|
||||
public SolutionDrainTransferDoAfterEvent(FixedPoint2 amount)
|
||||
{
|
||||
Amount = amount;
|
||||
}
|
||||
|
||||
public override DoAfterEvent Clone() => this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Doafter event for solution transfers where the held item is filled from the target. Checks for validity both when initiating and when finishing the event.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class SolutionRefillTransferDoAfterEvent : DoAfterEvent
|
||||
{
|
||||
public FixedPoint2 Amount;
|
||||
|
||||
public SolutionRefillTransferDoAfterEvent(FixedPoint2 amount)
|
||||
{
|
||||
Amount = amount;
|
||||
}
|
||||
|
||||
public override DoAfterEvent Clone() => this;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Content.Shared.Chemistry.Reaction
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
|
||||
|
||||
/// <summary>
|
||||
/// A cache of all reactions indexed by at most ONE of their required reactants.
|
||||
@@ -205,27 +206,12 @@ namespace Content.Shared.Chemistry.Reaction
|
||||
|
||||
private void OnReaction(Entity<SolutionComponent> soln, ReactionPrototype reaction, ReagentPrototype? reagent, FixedPoint2 unitReactions)
|
||||
{
|
||||
var args = new EntityEffectReagentArgs(soln, EntityManager, null, soln.Comp.Solution, unitReactions, reagent, null, 1f);
|
||||
|
||||
var posFound = _transformSystem.TryGetMapOrGridCoordinates(soln, out var gridPos);
|
||||
|
||||
_adminLogger.Add(LogType.ChemicalReaction, reaction.Impact,
|
||||
$"Chemical reaction {reaction.ID:reaction} occurred with strength {unitReactions:strength} on entity {ToPrettyString(soln):metabolizer} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found]")}");
|
||||
|
||||
foreach (var effect in reaction.Effects)
|
||||
{
|
||||
if (!effect.ShouldApply(args))
|
||||
continue;
|
||||
|
||||
if (effect.ShouldLog)
|
||||
{
|
||||
var entity = args.TargetEntity;
|
||||
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
|
||||
$"Reaction effect {effect.GetType().Name:effect} of reaction {reaction.ID:reaction} applied on entity {ToPrettyString(entity):entity} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}");
|
||||
}
|
||||
|
||||
effect.Effect(args);
|
||||
}
|
||||
_entityEffects.ApplyEffects(soln, reaction.Effects, unitReactions.Float());
|
||||
|
||||
// Someday, some brave soul will thread through an optional actor
|
||||
// argument in from every call of OnReaction up, all just to pass
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Content.Shared.Chemistry.Reaction
|
||||
/// <summary>
|
||||
/// Effects to be triggered when the reaction occurs.
|
||||
/// </summary>
|
||||
[DataField("effects")] public List<EntityEffect> Effects = new();
|
||||
[DataField("effects")] public EntityEffect[] Effects = [];
|
||||
|
||||
/// <summary>
|
||||
/// How dangerous is this effect? Stuff like bicaridine should be low, while things like methamphetamine
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed partial class ReactiveReagentEffectEntry
|
||||
public HashSet<string>? Reagents = null;
|
||||
|
||||
[DataField("effects", required: true)]
|
||||
public List<EntityEffect> Effects = default!;
|
||||
public EntityEffect[] Effects = default!;
|
||||
|
||||
[DataField("groups", readOnly: true, serverOnly: true,
|
||||
customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<ReactionMethod>, ReactiveGroupPrototype>))]
|
||||
|
||||
@@ -1,108 +1,35 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Shared.Chemistry;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ReactiveSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
|
||||
public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method)
|
||||
{
|
||||
foreach (var reagent in solution.Contents.ToArray())
|
||||
{
|
||||
ReactionEntity(uid, method, reagent, solution);
|
||||
ReactionEntity(uid, method, reagent);
|
||||
}
|
||||
}
|
||||
|
||||
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source)
|
||||
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity)
|
||||
{
|
||||
// We throw if the reagent specified doesn't exist.
|
||||
var proto = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
|
||||
ReactionEntity(uid, method, proto, reagentQuantity, source);
|
||||
}
|
||||
|
||||
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto,
|
||||
ReagentQuantity reagentQuantity, Solution? source)
|
||||
{
|
||||
if (!TryComp(uid, out ReactiveComponent? reactive))
|
||||
if (reagentQuantity.Quantity == FixedPoint2.Zero)
|
||||
return;
|
||||
|
||||
// custom event for bypassing reactivecomponent stuff
|
||||
var ev = new ReactionEntityEvent(method, proto, reagentQuantity, source);
|
||||
// We throw if the reagent specified doesn't exist.
|
||||
if (!_proto.Resolve<ReagentPrototype>(reagentQuantity.Reagent.Prototype, out var proto))
|
||||
return;
|
||||
|
||||
var ev = new ReactionEntityEvent(method, reagentQuantity, proto);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
// If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified.
|
||||
var args = new EntityEffectReagentArgs(uid, EntityManager, null, source, source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, proto, method, 1f);
|
||||
|
||||
// First, check if the reagent wants to apply any effects.
|
||||
if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null)
|
||||
{
|
||||
foreach (var (key, val) in proto.ReactiveEffects)
|
||||
{
|
||||
if (!val.Methods.Contains(method))
|
||||
continue;
|
||||
|
||||
if (!reactive.ReactiveGroups.ContainsKey(key))
|
||||
continue;
|
||||
|
||||
if (!reactive.ReactiveGroups[key].Contains(method))
|
||||
continue;
|
||||
|
||||
foreach (var effect in val.Effects)
|
||||
{
|
||||
if (!effect.ShouldApply(args, _robustRandom))
|
||||
continue;
|
||||
|
||||
if (effect.ShouldLog)
|
||||
{
|
||||
var entity = args.TargetEntity;
|
||||
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
|
||||
$"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}");
|
||||
}
|
||||
|
||||
effect.Effect(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then, check if the prototype has any effects it can apply as well.
|
||||
if (reactive.Reactions != null)
|
||||
{
|
||||
foreach (var entry in reactive.Reactions)
|
||||
{
|
||||
if (!entry.Methods.Contains(method))
|
||||
continue;
|
||||
|
||||
if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID))
|
||||
continue;
|
||||
|
||||
foreach (var effect in entry.Effects)
|
||||
{
|
||||
if (!effect.ShouldApply(args, _robustRandom))
|
||||
continue;
|
||||
|
||||
if (effect.ShouldLog)
|
||||
{
|
||||
var entity = args.TargetEntity;
|
||||
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
|
||||
$"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}");
|
||||
}
|
||||
|
||||
effect.Effect(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public enum ReactionMethod
|
||||
@@ -113,9 +40,4 @@ Ingestion,
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly record struct ReactionEntityEvent(
|
||||
ReactionMethod Method,
|
||||
ReagentPrototype Reagent,
|
||||
ReagentQuantity ReagentQuantity,
|
||||
Solution? Source
|
||||
);
|
||||
public readonly record struct ReactionEntityEvent(ReactionMethod Method, ReagentQuantity ReagentQuantity, ReagentPrototype Reagent);
|
||||
|
||||
@@ -2,21 +2,17 @@ using System.Collections.Frozen;
|
||||
using System.Linq;
|
||||
using Content.Shared.FixedPoint;
|
||||
using System.Text.Json.Serialization;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Contraband;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Nutrition;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Slippery;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -190,6 +186,7 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
[DataField]
|
||||
public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepPuddle");
|
||||
|
||||
// TODO: Reaction tile doesn't work properly and destroys reagents way too quickly
|
||||
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityManager entityManager, List<ReagentData>? data)
|
||||
{
|
||||
var removed = FixedPoint2.Zero;
|
||||
@@ -211,33 +208,32 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void ReactionPlant(EntityUid? plantHolder,
|
||||
ReagentQuantity amount,
|
||||
Solution solution,
|
||||
EntityManager entityManager,
|
||||
IRobustRandom random,
|
||||
ISharedAdminLogManager logger)
|
||||
public IEnumerable<string> GuidebookReagentEffectsDescription(IPrototypeManager prototype, IEntitySystemManager entSys, IEnumerable<EntityEffect> effects, FixedPoint2? metabolism = null)
|
||||
{
|
||||
if (plantHolder == null)
|
||||
return;
|
||||
return effects.Select(x => GuidebookReagentEffectDescription(prototype, entSys, x, metabolism))
|
||||
.Where(x => x is not null)
|
||||
.Select(x => x!)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var args = new EntityEffectReagentArgs(plantHolder.Value, entityManager, null, solution, amount.Quantity, this, null, 1f);
|
||||
foreach (var plantMetabolizable in PlantMetabolisms)
|
||||
{
|
||||
if (!plantMetabolizable.ShouldApply(args, random))
|
||||
continue;
|
||||
public string? GuidebookReagentEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys, EntityEffect effect, FixedPoint2? metabolism)
|
||||
{
|
||||
if (effect.EntityEffectGuidebookText(prototype, entSys) is not { } description)
|
||||
return null;
|
||||
|
||||
if (plantMetabolizable.ShouldLog)
|
||||
{
|
||||
var entity = args.TargetEntity;
|
||||
logger.Add(
|
||||
LogType.ReagentEffect,
|
||||
plantMetabolizable.LogImpact,
|
||||
$"Plant metabolism effect {plantMetabolizable.GetType().Name:effect} of reagent {ID} applied on entity {entity}");
|
||||
}
|
||||
var quantity = metabolism == null ? 0f : (double) (effect.MinScale * metabolism);
|
||||
|
||||
plantMetabolizable.Effect(args);
|
||||
}
|
||||
return Loc.GetString(
|
||||
"guidebook-reagent-effect-description",
|
||||
("reagent", LocalizedName),
|
||||
("quantity", quantity),
|
||||
("effect", description),
|
||||
("chance", effect.Probability),
|
||||
("conditionCount", effect.Conditions?.Length ?? 0),
|
||||
("conditions",
|
||||
ContentLocalizationManager.FormatList(
|
||||
effect.Conditions?.Select(x => x.EntityConditionGuidebookText(prototype)).ToList() ?? new List<string>()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +242,7 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
{
|
||||
public string ReagentPrototype;
|
||||
|
||||
// TODO: Kill Metabolism groups!
|
||||
public Dictionary<ProtoId<MetabolismGroupPrototype>, ReagentEffectsGuideEntry>? GuideEntries;
|
||||
|
||||
public List<string>? PlantMetabolisms = null;
|
||||
@@ -254,15 +251,12 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
{
|
||||
ReagentPrototype = proto.ID;
|
||||
GuideEntries = proto.Metabolisms?
|
||||
.Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys)))
|
||||
.Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys, proto)))
|
||||
.ToDictionary(x => x.Key, x => x.Item2);
|
||||
if (proto.PlantMetabolisms.Count > 0)
|
||||
{
|
||||
PlantMetabolisms = new List<string>(proto.PlantMetabolisms
|
||||
.Select(x => x.GuidebookEffectDescription(prototype, entSys))
|
||||
.Where(x => x is not null)
|
||||
.Select(x => x!)
|
||||
.ToArray());
|
||||
PlantMetabolisms =
|
||||
new List<string>(proto.GuidebookReagentEffectsDescription(prototype, entSys, proto.PlantMetabolisms));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,15 +285,11 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
[DataField("effects", required: true)]
|
||||
public EntityEffect[] Effects = default!;
|
||||
|
||||
public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
public string EntityEffectFormat => "guidebook-reagent-effect-description";
|
||||
|
||||
public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys, ReagentPrototype proto)
|
||||
{
|
||||
return new ReagentEffectsGuideEntry(MetabolismRate,
|
||||
Effects
|
||||
.Select(x => x.GuidebookEffectDescription(prototype, entSys)) // hate.
|
||||
.Concat(StatusEffects.Select(x => x.Describe(prototype, entSys))) // Offbrand
|
||||
.Where(x => x is not null)
|
||||
.Select(x => x!)
|
||||
.ToArray());
|
||||
return new ReagentEffectsGuideEntry(MetabolismRate, proto.GuidebookReagentEffectsDescription(prototype, entSys, Effects, MetabolismRate).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Climbing.Components;
|
||||
using Content.Shared.Climbing.Events;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.Hands.Components;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Cloning;
|
||||
|
||||
public abstract partial class SharedCloningSystem : EntitySystem
|
||||
@@ -11,4 +13,14 @@ public abstract partial class SharedCloningSystem : EntitySystem
|
||||
public virtual void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy components from one entity to another based on a CloningSettingsPrototype.
|
||||
/// </summary>
|
||||
/// <param name="original">The orignal Entity to clone components from.</param>
|
||||
/// <param name="clone">The target Entity to clone components to.</param>
|
||||
/// <param name="settings">The clone settings prototype id containing the list of components to clone.</param>
|
||||
public virtual void CloneComponents(EntityUid original, EntityUid clone, ProtoId<CloningSettingsPrototype> settings)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,27 +62,7 @@ public sealed class MaskSystem : EntitySystem
|
||||
|
||||
private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args)
|
||||
{
|
||||
if (!mask.IsToggled || !mask.IsToggleable)
|
||||
return;
|
||||
|
||||
mask.IsToggled = false;
|
||||
ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after setting IsToggled, raises events and dirties.
|
||||
/// </summary>
|
||||
private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false)
|
||||
{
|
||||
Dirty(uid, mask);
|
||||
if (mask.ToggleActionEntity is { } action)
|
||||
_actionSystem.SetToggled(action, mask.IsToggled);
|
||||
|
||||
var maskEv = new ItemMaskToggledEvent((uid, mask), wearer);
|
||||
RaiseLocalEvent(uid, ref maskEv);
|
||||
|
||||
var wearerEv = new WearerMaskToggledEvent((uid, mask));
|
||||
RaiseLocalEvent(wearer, ref wearerEv);
|
||||
SetToggled(uid, false);
|
||||
}
|
||||
|
||||
private void OnFolded(Entity<MaskComponent> ent, ref FoldedEvent args)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Systems;
|
||||
|
||||
@@ -2,7 +2,7 @@ using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry.Hypospray.Events;
|
||||
using Content.Shared.Climbing.Components;
|
||||
using Content.Shared.Climbing.Events;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Medical;
|
||||
using Content.Shared.Popups;
|
||||
@@ -49,7 +49,7 @@ public sealed class ClumsySystem : EntitySystem
|
||||
return;
|
||||
|
||||
// TODO: Replace with RandomPredicted once the engine PR is merged
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
|
||||
var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id);
|
||||
var rand = new System.Random(seed);
|
||||
if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
|
||||
return;
|
||||
@@ -68,7 +68,7 @@ public sealed class ClumsySystem : EntitySystem
|
||||
return;
|
||||
|
||||
// TODO: Replace with RandomPredicted once the engine PR is merged
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
|
||||
var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id);
|
||||
var rand = new System.Random(seed);
|
||||
if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
|
||||
return;
|
||||
@@ -87,7 +87,7 @@ public sealed class ClumsySystem : EntitySystem
|
||||
return;
|
||||
|
||||
// TODO: Replace with RandomPredicted once the engine PR is merged
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Item).Id });
|
||||
var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(args.Item).Id);
|
||||
var rand = new System.Random(seed);
|
||||
if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
|
||||
return;
|
||||
@@ -95,7 +95,7 @@ public sealed class ClumsySystem : EntitySystem
|
||||
args.Cancelled = true; // fail to catch
|
||||
|
||||
if (ent.Comp.CatchingFailDamage != null)
|
||||
_damageable.TryChangeDamage(ent, ent.Comp.CatchingFailDamage, origin: args.Item);
|
||||
_damageable.ChangeDamage(ent.Owner, ent.Comp.CatchingFailDamage, origin: args.Item);
|
||||
|
||||
// Collisions don't work properly with PopupPredicted or PlayPredicted.
|
||||
// So we make this server only.
|
||||
@@ -121,13 +121,13 @@ public sealed class ClumsySystem : EntitySystem
|
||||
return;
|
||||
|
||||
// TODO: Replace with RandomPredicted once the engine PR is merged
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Gun).Id });
|
||||
var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(args.Gun).Id);
|
||||
var rand = new System.Random(seed);
|
||||
if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
|
||||
return;
|
||||
|
||||
if (ent.Comp.GunShootFailDamage != null)
|
||||
_damageable.TryChangeDamage(ent, ent.Comp.GunShootFailDamage, origin: ent);
|
||||
_damageable.ChangeDamage(ent.Owner, ent.Comp.GunShootFailDamage, origin: ent);
|
||||
|
||||
_stun.TryUpdateParalyzeDuration(ent, ent.Comp.GunShootFailStunTime);
|
||||
|
||||
@@ -146,7 +146,7 @@ public sealed class ClumsySystem : EntitySystem
|
||||
return;
|
||||
|
||||
// TODO: Replace with RandomPredicted once the engine PR is merged
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
|
||||
var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id);
|
||||
var rand = new System.Random(seed);
|
||||
if (!_cfg.GetCVar(CCVars.GameTableBonk) && !rand.Prob(ent.Comp.ClumsyDefaultCheck))
|
||||
return;
|
||||
@@ -199,7 +199,7 @@ public sealed class ClumsySystem : EntitySystem
|
||||
{
|
||||
stunTime = bonkComp.BonkTime;
|
||||
if (bonkComp.BonkDamage != null)
|
||||
_damageable.TryChangeDamage(target, bonkComp.BonkDamage, true);
|
||||
_damageable.ChangeDamage(target.Owner, bonkComp.BonkDamage, true);
|
||||
}
|
||||
|
||||
_stun.TryUpdateParalyzeDuration(target, stunTime);
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -65,6 +66,10 @@ public sealed class PacificationSystem : EntitySystem
|
||||
if (HasComp<PacifismAllowedGunComponent>(args.Used))
|
||||
return;
|
||||
|
||||
if (TryComp<BatteryWeaponFireModesComponent>(args.Used, out var component))
|
||||
if (component.FireModes[component.CurrentFireMode].PacifismAllowedMode)
|
||||
return;
|
||||
|
||||
// Disallow firing guns in all cases.
|
||||
ShowPopup(ent, args.Used, "pacified-cannot-fire-gun");
|
||||
args.Cancel();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Construction.Components;
|
||||
|
||||
@@ -573,7 +573,7 @@ namespace Content.Shared.Containers.ItemSlots
|
||||
item = slot.Item;
|
||||
|
||||
// This handles user logic
|
||||
if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value))
|
||||
if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value, showPopup: true))
|
||||
return false;
|
||||
|
||||
Eject(uid, slot, item!.Value, user, excludeUserAudio);
|
||||
|
||||
@@ -45,6 +45,8 @@ public abstract class SharedCriminalRecordsSystem : EntitySystem
|
||||
SecurityStatus.Detained => "SecurityIconIncarcerated",
|
||||
SecurityStatus.Discharged => "SecurityIconDischarged",
|
||||
SecurityStatus.Suspected => "SecurityIconSuspected",
|
||||
SecurityStatus.Hostile => "SecurityIconHostile",
|
||||
SecurityStatus.Eliminated => "SecurityIconEliminated",
|
||||
_ => record.StatusIcon
|
||||
};
|
||||
|
||||
|
||||
@@ -24,12 +24,6 @@ public sealed partial class CuffableComponent : Component
|
||||
[ViewVariables]
|
||||
public int CuffedHandCount => Container.ContainedEntities.Count * 2;
|
||||
|
||||
/// <summary>
|
||||
/// The last pair of cuffs that was added to this entity.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
|
||||
|
||||
/// <summary>
|
||||
/// Container of various handcuffs currently applied to the entity.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration.Components;
|
||||
@@ -260,7 +261,7 @@ namespace Content.Shared.Cuffs
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
TryUncuff(ent, ent, cuffable: ent.Comp);
|
||||
TryUncuff((ent, ent.Comp), ent);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
@@ -278,7 +279,7 @@ namespace Content.Shared.Cuffs
|
||||
|
||||
Verb verb = new()
|
||||
{
|
||||
Act = () => TryUncuff(uid, args.User, cuffable: component),
|
||||
Act = () => TryUncuff((uid, component), args.User),
|
||||
DoContactInteraction = true,
|
||||
Text = Loc.GetString("uncuff-verb-get-data-text")
|
||||
};
|
||||
@@ -585,41 +586,31 @@ namespace Content.Shared.Cuffs
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryUncuff(Entity{CuffableComponent?},EntityUid,Entity{HandcuffComponent?})"/>
|
||||
public void TryUncuff(Entity<CuffableComponent?> target, EntityUid user)
|
||||
{
|
||||
if (!TryGetLastCuff(target, out var cuff))
|
||||
return;
|
||||
|
||||
TryUncuff(target, user, cuff.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
|
||||
/// If the uncuffing succeeds, the cuffs will drop on the floor.
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
/// <param name="user">The cuffed entity</param>
|
||||
/// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
|
||||
/// <param name="cuffable"></param>
|
||||
/// <param name="cuff"></param>
|
||||
public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove = null, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
|
||||
/// <param name="target">The entity we're trying to remove cuffs from.</param>
|
||||
/// <param name="user">The entity doing the cuffing.</param>
|
||||
/// <param name="cuff">The handcuff entity we're attempting to remove.</param>
|
||||
public void TryUncuff(Entity<CuffableComponent?> target, EntityUid user, Entity<HandcuffComponent?> cuff)
|
||||
{
|
||||
if (!Resolve(target, ref cuffable))
|
||||
if (!Resolve(target, ref target.Comp) || !Resolve(cuff, ref cuff.Comp))
|
||||
return;
|
||||
|
||||
var isOwner = user == target;
|
||||
var isOwner = user == target.Owner;
|
||||
|
||||
if (cuffsToRemove == null)
|
||||
{
|
||||
if (cuffable.Container.ContainedEntities.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cuffsToRemove = cuffable.LastAddedCuffs;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!cuffable.Container.ContainedEntities.Contains(cuffsToRemove.Value))
|
||||
{
|
||||
Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
|
||||
}
|
||||
}
|
||||
|
||||
if (!Resolve(cuffsToRemove.Value, ref cuff))
|
||||
return;
|
||||
if (!target.Comp.Container.ContainedEntities.Contains(cuff))
|
||||
Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
|
||||
|
||||
var attempt = new UncuffAttemptEvent(user, target);
|
||||
RaiseLocalEvent(user, ref attempt, true);
|
||||
@@ -629,29 +620,28 @@ namespace Content.Shared.Cuffs
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwner && !_interaction.InRangeUnobstructed(user, target))
|
||||
if (!isOwner && !_interaction.InRangeUnobstructed(user, target.Owner))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.BreakoutTime : cuff.UncuffTime);
|
||||
var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.Comp.BreakoutTime : cuff.Comp.UncuffTime);
|
||||
RaiseLocalEvent(user, ref ev);
|
||||
var uncuffTime = ev.Duration;
|
||||
|
||||
if (isOwner)
|
||||
{
|
||||
if (!TryComp(cuffsToRemove.Value, out UseDelayComponent? useDelay))
|
||||
if (!TryComp(cuff, out UseDelayComponent? useDelay))
|
||||
return;
|
||||
|
||||
if (!_delay.TryResetDelay((cuffsToRemove.Value, useDelay), true))
|
||||
if (!_delay.TryResetDelay((cuff, useDelay), true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuffsToRemove)
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuff)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
BreakOnWeightlessMove = false,
|
||||
@@ -666,7 +656,7 @@ namespace Content.Shared.Cuffs
|
||||
|
||||
_adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):player} is trying to uncuff {ToPrettyString(target):subject}");
|
||||
|
||||
var popupText = user == target
|
||||
var popupText = user == target.Owner
|
||||
? "cuffable-component-start-uncuffing-self-observer"
|
||||
: "cuffable-component-start-uncuffing-observer";
|
||||
_popup.PopupEntity(
|
||||
@@ -678,7 +668,7 @@ namespace Content.Shared.Cuffs
|
||||
.RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user),
|
||||
true);
|
||||
|
||||
if (target == user)
|
||||
if (isOwner)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-self"), user, user);
|
||||
}
|
||||
@@ -694,7 +684,7 @@ namespace Content.Shared.Cuffs
|
||||
target);
|
||||
}
|
||||
|
||||
_audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user);
|
||||
_audio.PlayPredicted(isOwner ? cuff.Comp.StartBreakoutSound : cuff.Comp.StartUncuffSound, target, user);
|
||||
}
|
||||
|
||||
public void Uncuff(EntityUid target, EntityUid? user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
|
||||
@@ -818,9 +808,56 @@ namespace Content.Shared.Cuffs
|
||||
|
||||
#endregion
|
||||
|
||||
public IReadOnlyList<EntityUid> GetAllCuffs(CuffableComponent component)
|
||||
/// <summary>
|
||||
/// Tries to get a list of all the handcuffs stored in an entity's <see cref="CuffableComponent"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The cuffable entity in question.</param>
|
||||
/// <param name="cuffs">A list of cuffs if it exists.</param>
|
||||
/// <returns>True if a list of cuffs with cuffs exists. False if no list exists or if it is empty.</returns>
|
||||
public bool TryGetAllCuffs(Entity<CuffableComponent?> entity, out IReadOnlyList<EntityUid> cuffs)
|
||||
{
|
||||
return component.Container.ContainedEntities;
|
||||
cuffs = GetAllCuffs(entity);
|
||||
|
||||
return cuffs.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a list of all the handcuffs stored in a entity's <see cref="CuffableComponent"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The cuffable entity in question.</param>
|
||||
/// <returns>A list of cuffs if it exists, or null if there are no cuffs.</returns>
|
||||
public IReadOnlyList<EntityUid> GetAllCuffs(Entity<CuffableComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return [];
|
||||
|
||||
return entity.Comp.Container.ContainedEntities;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the most recently added pair of handcuffs added to an entity with <see cref="CuffableComponent"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The cuffable entity in question.</param>
|
||||
/// <param name="cuff">The most recently added cuff.</param>
|
||||
/// <returns>Returns true if a cuff exists and false if one doesn't.</returns>
|
||||
public bool TryGetLastCuff(Entity<CuffableComponent?> entity, [NotNullWhen(true)] out EntityUid? cuff)
|
||||
{
|
||||
cuff = GetLastCuffOrNull(entity);
|
||||
|
||||
return cuff != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the most recently added pair of handcuffs added to an entity with <see cref="CuffableComponent"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The cuffable entity in question.</param>
|
||||
/// <returns>The most recently added cuff or null if none exists.</returns>
|
||||
public EntityUid? GetLastCuffOrNull(Entity<CuffableComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return null;
|
||||
|
||||
return entity.Comp.Container.ContainedEntities.Count == 0 ? null : entity.Comp.Container.ContainedEntities.Last();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Damage.Components;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.StatusIcon;
|
||||
@@ -6,105 +7,100 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
namespace Content.Shared.Damage.Components;
|
||||
|
||||
/// <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="DamageModifierSetPrototype"/>.
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent]
|
||||
[Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)]
|
||||
public sealed partial class DamageableComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that allows entities to take damage.
|
||||
/// This <see cref="DamageContainerPrototype"/> specifies what damage types are supported by this component.
|
||||
/// If null, all damage types will be supported.
|
||||
/// </summary>
|
||||
[DataField("damageContainer")]
|
||||
// ReSharper disable once InconsistentNaming - This is wrong but fixing it is potentially annoying for downstreams.
|
||||
public ProtoId<DamageContainerPrototype>? DamageContainerID;
|
||||
|
||||
/// <summary>
|
||||
/// This <see cref="DamageModifierSetPrototype"/> will be applied to any damage that is dealt to this container,
|
||||
/// unless the damage explicitly ignores resistances.
|
||||
/// </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="DamageModifierSetPrototype"/>.
|
||||
/// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here
|
||||
/// to reduce duplication.
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent]
|
||||
[Access(typeof(DamageableSystem), Other = AccessPermissions.ReadExecute)]
|
||||
public sealed partial class DamageableComponent : Component
|
||||
[DataField("damageModifierSet")]
|
||||
public ProtoId<DamageModifierSetPrototype>? DamageModifierSetId;
|
||||
|
||||
/// <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(readOnly: true)] //TODO FULL GAME SAVE
|
||||
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, FixedPoint2> DamagePerGroup = new();
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all damages in the DamageableComponent.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public FixedPoint2 TotalDamage;
|
||||
|
||||
[DataField("radiationDamageTypes")]
|
||||
// ReSharper disable once UseCollectionExpression - Cannot refactor this as it's a potential sandbox violation.
|
||||
public List<ProtoId<DamageTypePrototype>> RadiationDamageTypeIDs = new() { "Radiation" };
|
||||
|
||||
/// <summary>
|
||||
/// Group types that affect the pain overlay.
|
||||
/// </summary>
|
||||
/// TODO: Add support for adding damage types specifically rather than whole damage groups
|
||||
[DataField]
|
||||
// ReSharper disable once UseCollectionExpression - Cannot refactor this as it's a potential sandbox volation.
|
||||
public List<ProtoId<DamageGroupPrototype>> PainDamageGroups = new() { "Brute", "Burn" };
|
||||
|
||||
[DataField]
|
||||
public Dictionary<MobState, ProtoId<HealthIconPrototype>> HealthIcons = new()
|
||||
{
|
||||
/// <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")]
|
||||
public ProtoId<DamageContainerPrototype>? DamageContainerID;
|
||||
{ MobState.Alive, "HealthIconFine" },
|
||||
{ MobState.Critical, "HealthIconCritical" },
|
||||
{ MobState.Dead, "HealthIconDead" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// This <see cref="DamageModifierSetPrototype"/> will be applied to any damage that is dealt to this container,
|
||||
/// unless the damage explicitly ignores resistances.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Though DamageModifierSets can be deserialized directly, we only want to use the prototype version here
|
||||
/// to reduce duplication.
|
||||
/// </remarks>
|
||||
[DataField("damageModifierSet")]
|
||||
public ProtoId<DamageModifierSetPrototype>? DamageModifierSetId;
|
||||
[DataField]
|
||||
public ProtoId<HealthIconPrototype> RottingIcon = "HealthIconRotting";
|
||||
|
||||
/// <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(readOnly: true)] // TODO FULL GAME SAVE
|
||||
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, FixedPoint2> DamagePerGroup = new();
|
||||
|
||||
/// <summary>
|
||||
/// The sum of all damages in the DamageableComponent.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public FixedPoint2 TotalDamage;
|
||||
|
||||
[DataField("radiationDamageTypes")]
|
||||
public List<ProtoId<DamageTypePrototype>> RadiationDamageTypeIDs = new() { "Radiation" };
|
||||
|
||||
/// <summary>
|
||||
/// Group types that affect the pain overlay.
|
||||
/// </summary>
|
||||
/// TODO: Add support for adding damage types specifically rather than whole damage groups
|
||||
[DataField]
|
||||
public List<ProtoId<DamageGroupPrototype>> PainDamageGroups = new() { "Brute", "Burn" };
|
||||
|
||||
[DataField]
|
||||
public Dictionary<MobState, ProtoId<HealthIconPrototype>> HealthIcons = new()
|
||||
{
|
||||
{ MobState.Alive, "HealthIconFine" },
|
||||
{ MobState.Critical, "HealthIconCritical" },
|
||||
{ MobState.Dead, "HealthIconDead" },
|
||||
};
|
||||
|
||||
[DataField]
|
||||
public ProtoId<HealthIconPrototype> RottingIcon = "HealthIconRotting";
|
||||
|
||||
[DataField]
|
||||
public FixedPoint2? HealthBarThreshold;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class DamageableComponentState : ComponentState
|
||||
{
|
||||
public readonly Dictionary<string, FixedPoint2> DamageDict;
|
||||
public readonly string? DamageContainerId;
|
||||
public readonly string? ModifierSetId;
|
||||
public readonly FixedPoint2? HealthBarThreshold;
|
||||
|
||||
public DamageableComponentState(
|
||||
Dictionary<string, FixedPoint2> damageDict,
|
||||
string? damageContainerId,
|
||||
string? modifierSetId,
|
||||
FixedPoint2? healthBarThreshold)
|
||||
{
|
||||
DamageDict = damageDict;
|
||||
DamageContainerId = damageContainerId;
|
||||
ModifierSetId = modifierSetId;
|
||||
HealthBarThreshold = healthBarThreshold;
|
||||
}
|
||||
}
|
||||
[DataField]
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Damage.Components;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Damage.Components;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Damage.Prototypes
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Damage.Prototypes
|
||||
{
|
||||
|
||||
@@ -73,9 +73,9 @@ public sealed class DamageOnAttackedSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, entity.Comp.IgnoreResistances, origin: entity);
|
||||
totalDamage = _damageableSystem.ChangeDamage(args.User, totalDamage, entity.Comp.IgnoreResistances, origin: entity);
|
||||
|
||||
if (totalDamage != null && totalDamage.AnyPositive())
|
||||
if (totalDamage.AnyPositive())
|
||||
{
|
||||
_adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} injured themselves by attacking {ToPrettyString(entity):target} and received {totalDamage.GetTotal():damage} damage");
|
||||
_audioSystem.PlayPredicted(entity.Comp.InteractSound, entity, args.User);
|
||||
|
||||
@@ -65,7 +65,7 @@ public sealed class DamageOnInteractSystem : EntitySystem
|
||||
// or checking the entity for the comp itself if the inventory didn't work
|
||||
if (protectiveEntity.Comp == null && TryComp<DamageOnInteractProtectionComponent>(args.User, out var protectiveComp))
|
||||
protectiveEntity = (args.User, protectiveComp);
|
||||
|
||||
|
||||
|
||||
// if protectiveComp isn't null after all that, it means the user has protection,
|
||||
// so let's calculate how much they resist
|
||||
@@ -75,9 +75,9 @@ public sealed class DamageOnInteractSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target);
|
||||
totalDamage = _damageableSystem.ChangeDamage(args.User, totalDamage, origin: args.Target);
|
||||
|
||||
if (totalDamage != null && totalDamage.AnyPositive())
|
||||
if (totalDamage.AnyPositive())
|
||||
{
|
||||
// Record this interaction and determine when a user is allowed to interact with this entity again
|
||||
entity.Comp.LastInteraction = _gameTiming.CurTime;
|
||||
|
||||
235
Content.Shared/Damage/Systems/DamageableSystem.API.cs
Normal file
235
Content.Shared/Damage/Systems/DamageableSystem.API.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public sealed partial class DamageableSystem
|
||||
{
|
||||
/// <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(Entity<DamageableComponent?> ent, DamageSpecifier damage)
|
||||
{
|
||||
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
ent.Comp.Damage = damage;
|
||||
|
||||
OnEntityDamageChanged((ent, ent.Comp));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// If the attempt was successful or not.
|
||||
/// </returns>
|
||||
public bool TryChangeDamage(
|
||||
Entity<DamageableComponent?> ent,
|
||||
DamageSpecifier damage,
|
||||
bool ignoreResistances = false,
|
||||
bool interruptsDoAfters = true,
|
||||
EntityUid? origin = null,
|
||||
bool ignoreGlobalModifiers = false
|
||||
)
|
||||
{
|
||||
//! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty.
|
||||
// If you deal 0.0 of some damage type, Empty will be false!
|
||||
return !TryChangeDamage(ent, damage, out _, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// If the attempt was successful or not.
|
||||
/// </returns>
|
||||
public bool TryChangeDamage(
|
||||
Entity<DamageableComponent?> ent,
|
||||
DamageSpecifier damage,
|
||||
out DamageSpecifier newDamage,
|
||||
bool ignoreResistances = false,
|
||||
bool interruptsDoAfters = true,
|
||||
EntityUid? origin = null,
|
||||
bool ignoreGlobalModifiers = false
|
||||
)
|
||||
{
|
||||
//! Empty just checks if the DamageSpecifier is _literally_ empty, as in, is internal dictionary of damage types is empty.
|
||||
// If you deal 0.0 of some damage type, Empty will be false!
|
||||
newDamage = ChangeDamage(ent, damage, ignoreResistances, interruptsDoAfters, origin, ignoreGlobalModifiers);
|
||||
return !damage.Empty;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// The actual amount of damage taken, as a DamageSpecifier.
|
||||
/// </returns>
|
||||
public DamageSpecifier ChangeDamage(
|
||||
Entity<DamageableComponent?> ent,
|
||||
DamageSpecifier damage,
|
||||
bool ignoreResistances = false,
|
||||
bool interruptsDoAfters = true,
|
||||
EntityUid? origin = null,
|
||||
bool ignoreGlobalModifiers = false
|
||||
)
|
||||
{
|
||||
var damageDone = new DamageSpecifier();
|
||||
|
||||
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
|
||||
return damageDone;
|
||||
|
||||
if (damage.Empty)
|
||||
return damageDone;
|
||||
|
||||
var before = new BeforeDamageChangedEvent(damage, origin);
|
||||
RaiseLocalEvent(ent, ref before);
|
||||
|
||||
if (before.Cancelled)
|
||||
return damageDone;
|
||||
|
||||
// Apply resistances
|
||||
if (!ignoreResistances)
|
||||
{
|
||||
if (
|
||||
ent.Comp.DamageModifierSetId != null &&
|
||||
_prototypeManager.Resolve(ent.Comp.DamageModifierSetId, out var modifierSet)
|
||||
)
|
||||
damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
|
||||
|
||||
// TODO DAMAGE
|
||||
// byref struct event.
|
||||
var ev = new DamageModifyEvent(damage, origin);
|
||||
RaiseLocalEvent(ent, ev);
|
||||
damage = ev.Damage;
|
||||
|
||||
if (damage.Empty)
|
||||
return damageDone;
|
||||
}
|
||||
|
||||
if (!ignoreGlobalModifiers)
|
||||
damage = ApplyUniversalAllModifiers(damage);
|
||||
|
||||
|
||||
damageDone.DamageDict.EnsureCapacity(damage.DamageDict.Count);
|
||||
|
||||
var dict = ent.Comp.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;
|
||||
damageDone.DamageDict[type] = newValue - oldValue;
|
||||
}
|
||||
|
||||
if (!damageDone.Empty)
|
||||
OnEntityDamageChanged((ent, ent.Comp), damageDone, interruptsDoAfters, origin);
|
||||
|
||||
return damageDone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the two universal "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 (
|
||||
MathHelper.CloseToPercent(UniversalAllDamageModifier, 1f) &&
|
||||
MathHelper.CloseToPercent(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;
|
||||
}
|
||||
|
||||
public void ClearAllDamage(Entity<DamageableComponent?> ent)
|
||||
{
|
||||
SetAllDamage(ent, FixedPoint2.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all damage types supported by a <see cref="Components.DamageableComponent"/> to the specified value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does nothing If the given damage value is negative.
|
||||
/// </remarks>
|
||||
public void SetAllDamage(Entity<DamageableComponent?> ent, FixedPoint2 newValue)
|
||||
{
|
||||
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
if (newValue < 0)
|
||||
return;
|
||||
|
||||
foreach (var type in ent.Comp.Damage.DamageDict.Keys)
|
||||
{
|
||||
ent.Comp.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.
|
||||
OnEntityDamageChanged((ent, ent.Comp), new DamageSpecifier());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set's the damage modifier set prototype for this entity.
|
||||
/// </summary>
|
||||
/// <param name="ent">The entity we're setting the modifier set of.</param>
|
||||
/// <param name="damageModifierSetId">The prototype we're setting.</param>
|
||||
public void SetDamageModifierSetId(Entity<DamageableComponent?> ent, ProtoId<DamageModifierSetPrototype>? damageModifierSetId)
|
||||
{
|
||||
if (!_damageableQuery.Resolve(ent, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
ent.Comp.DamageModifierSetId = damageModifierSetId;
|
||||
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public sealed partial class DamageableSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies damage to all entities to see how expensive it is to deal damage.
|
||||
/// </summary>
|
||||
public void ApplyDamageToAllEntities(List<Entity<DamageableComponent>> damageables, DamageSpecifier damage)
|
||||
{
|
||||
foreach (var (uid, damageable) in damageables)
|
||||
{
|
||||
TryChangeDamage((uid, damageable), damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
290
Content.Shared/Damage/Systems/DamageableSystem.Events.cs
Normal file
290
Content.Shared/Damage/Systems/DamageableSystem.Events.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Radiation.Events;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public sealed partial class DamageableSystem
|
||||
{
|
||||
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>();
|
||||
|
||||
// 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();
|
||||
_explosion.ReloadMap();
|
||||
},
|
||||
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;
|
||||
_explosion.ReloadMap();
|
||||
},
|
||||
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(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();
|
||||
}
|
||||
|
||||
private void OnIrradiated(Entity<DamageableComponent> ent, ref 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 ent.Comp.RadiationDamageTypeIDs)
|
||||
{
|
||||
damage.DamageDict.Add(typeId, damageValue);
|
||||
}
|
||||
|
||||
ChangeDamage(ent.Owner, damage, interruptsDoAfters: false, origin: args.Origin);
|
||||
}
|
||||
|
||||
private void OnRejuvenate(Entity<DamageableComponent> ent, ref RejuvenateEvent args)
|
||||
{
|
||||
// Do this so that the state changes when we set the damage
|
||||
_mobThreshold.SetAllowRevives(ent, true);
|
||||
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>
|
||||
/// 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(DamageSpecifier damage, EntityUid? origin = null)
|
||||
: EntityEventArgs, IInventoryRelayEvent
|
||||
{
|
||||
// Whenever locational damage is a thing, this should just check only that bit of armour.
|
||||
public SlotFlags TargetSlots => ~SlotFlags.POCKET;
|
||||
|
||||
public readonly DamageSpecifier OriginalDamage = damage;
|
||||
public DamageSpecifier Damage = damage;
|
||||
}
|
||||
|
||||
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 is null)
|
||||
return;
|
||||
|
||||
foreach (var damageChange in DamageDelta.DamageDict.Values)
|
||||
{
|
||||
if (damageChange <= 0)
|
||||
continue;
|
||||
|
||||
DamageIncreased = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
InterruptsDoAfters = interruptsDoAfters && DamageIncreased;
|
||||
}
|
||||
}
|
||||
@@ -1,484 +1,97 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Explosion.EntitySystems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
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
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public sealed partial class DamageableSystem : EntitySystem
|
||||
{
|
||||
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!;
|
||||
[Dependency] private readonly SharedExplosionSystem _explosion = default!;
|
||||
[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!;
|
||||
[Dependency] private readonly SharedExplosionSystem _explosion = default!;
|
||||
|
||||
private EntityQuery<AppearanceComponent> _appearanceQuery;
|
||||
private EntityQuery<DamageableComponent> _damageableQuery;
|
||||
private EntityQuery<AppearanceComponent> _appearanceQuery;
|
||||
private EntityQuery<DamageableComponent> _damageableQuery;
|
||||
|
||||
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, after: [typeof(Content.Shared.StatusEffectNew.StatusEffectsSystem)]); // Offbrand
|
||||
|
||||
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
|
||||
_damageableQuery = GetEntityQuery<DamageableComponent>();
|
||||
|
||||
// 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();
|
||||
_explosion.ReloadMap();
|
||||
}, 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;
|
||||
_explosion.ReloadMap();
|
||||
}, 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.Resolve<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, bool forcedRefresh = false) // Offbrand
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// TODO DAMAGE
|
||||
// byref struct event.
|
||||
RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin, forcedRefresh)); // Offbrand
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="ignoreResistances">If true, this will ignore the entity's damage modifier (<see cref="DamageableComponent.DamageModifierSetId"/> and skip raising a <see cref="DamageModifyEvent"/>.</param>
|
||||
/// <param name="interruptsDoAfters">Whether the damage should cancel any damage sensitive do-afters</param>
|
||||
/// <param name="origin">The entity that is causing this damage</param>
|
||||
/// <param name="ignoreGlobalModifiers">If true, this will skip over applying the universal damage modifiers (see <see cref="ApplyUniversalAllModifiers"/>).</param>
|
||||
/// <returns></returns>
|
||||
public DamageSpecifier? TryChangeDamage(
|
||||
EntityUid? uid,
|
||||
DamageSpecifier damage,
|
||||
bool ignoreResistances = false,
|
||||
bool interruptsDoAfters = true,
|
||||
DamageableComponent? damageable = null,
|
||||
EntityUid? origin = null,
|
||||
bool ignoreGlobalModifiers = false,
|
||||
bool forceRefresh = false) // Offbrand
|
||||
{
|
||||
if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false))
|
||||
{
|
||||
// TODO BODY SYSTEM pass damage onto body system
|
||||
// BOBBY WHEN?
|
||||
return null;
|
||||
}
|
||||
|
||||
if (damage.Empty && !forceRefresh) // Offbrand
|
||||
{
|
||||
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.Resolve(damageable.DamageModifierSetId, out var modifierSet))
|
||||
{
|
||||
damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
|
||||
}
|
||||
|
||||
// TODO DAMAGE
|
||||
// byref struct event.
|
||||
var ev = new DamageModifyEvent(damage, origin);
|
||||
RaiseLocalEvent(uid.Value, ev);
|
||||
damage = ev.Damage;
|
||||
|
||||
if (damage.Empty)
|
||||
{
|
||||
return damage;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoreGlobalModifiers)
|
||||
damage = ApplyUniversalAllModifiers(damage);
|
||||
|
||||
// Begin Offbrand
|
||||
var beforeCommit = new Content.Shared._Offbrand.Wounds.BeforeDamageCommitEvent(damage, forceRefresh);
|
||||
RaiseLocalEvent(uid.Value, ref beforeCommit);
|
||||
damage = beforeCommit.Damage;
|
||||
// End Offbrand
|
||||
|
||||
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, forceRefresh); // Offbrand
|
||||
|
||||
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)
|
||||
{
|
||||
Log.Debug("rejuvenate damage");
|
||||
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 = newDamage - component.Damage;
|
||||
delta.TrimZeros();
|
||||
|
||||
if (!delta.Empty)
|
||||
{
|
||||
component.Damage = newDamage;
|
||||
DamageChanged(uid, component, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Raised before damage is done, so stuff can cancel it if necessary.
|
||||
/// If the damage in a DamageableComponent was changed this function should be called.
|
||||
/// </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
|
||||
/// <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>
|
||||
private void OnEntityDamageChanged(
|
||||
Entity<DamageableComponent> ent,
|
||||
DamageSpecifier? damageDelta = null,
|
||||
bool interruptsDoAfters = true,
|
||||
EntityUid? origin = null
|
||||
)
|
||||
{
|
||||
// Whenever locational damage is a thing, this should just check only that bit of armour.
|
||||
public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET;
|
||||
ent.Comp.Damage.GetDamagePerGroup(_prototypeManager, ent.Comp.DamagePerGroup);
|
||||
ent.Comp.TotalDamage = ent.Comp.Damage.GetTotal();
|
||||
Dirty(ent);
|
||||
|
||||
public readonly DamageSpecifier OriginalDamage;
|
||||
public DamageSpecifier Damage;
|
||||
public EntityUid? Origin;
|
||||
|
||||
public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null)
|
||||
if (damageDelta != null && _appearanceQuery.TryGetComponent(ent, out var appearance))
|
||||
{
|
||||
OriginalDamage = damage;
|
||||
Damage = damage;
|
||||
Origin = origin;
|
||||
_appearance.SetData(
|
||||
ent,
|
||||
DamageVisualizerKeys.DamageUpdateGroups,
|
||||
new DamageVisualizerGroupData(ent.Comp.DamagePerGroup.Keys.ToList()),
|
||||
appearance
|
||||
);
|
||||
}
|
||||
|
||||
// TODO DAMAGE
|
||||
// byref struct event.
|
||||
RaiseLocalEvent(ent, new DamageChangedEvent(ent.Comp, damageDelta, interruptsDoAfters, origin));
|
||||
}
|
||||
|
||||
public sealed class DamageChangedEvent : EntityEventArgs
|
||||
private void DamageableGetState(Entity<DamageableComponent> ent, ref ComponentGetState args)
|
||||
{
|
||||
/// <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;
|
||||
|
||||
// Offbrand
|
||||
/// <summary>
|
||||
/// If this damage changed happened as part of a forced refresh
|
||||
/// </summary>
|
||||
public readonly bool ForcedRefresh;
|
||||
|
||||
public DamageChangedEvent(DamageableComponent damageable, DamageSpecifier? damageDelta, bool interruptsDoAfters, EntityUid? origin, bool forcedRefresh) // Offbrand
|
||||
if (_netMan.IsServer)
|
||||
{
|
||||
Damageable = damageable;
|
||||
DamageDelta = damageDelta;
|
||||
Origin = origin;
|
||||
ForcedRefresh = forcedRefresh; // Offbrand
|
||||
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 🫡
|
||||
|
||||
if (DamageDelta == null)
|
||||
return;
|
||||
|
||||
foreach (var damageChange in DamageDelta.DamageDict.Values)
|
||||
{
|
||||
if (damageChange > 0)
|
||||
{
|
||||
DamageIncreased = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
InterruptsDoAfters = interruptsDoAfters && DamageIncreased;
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Damage;
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public sealed class PassiveDamageSystem : EntitySystem
|
||||
{
|
||||
@@ -47,7 +45,7 @@ public sealed class PassiveDamageSystem : EntitySystem
|
||||
foreach (var allowedState in comp.AllowedStates)
|
||||
{
|
||||
if(allowedState == mobState.CurrentState)
|
||||
_damageable.TryChangeDamage(uid, comp.Damage, true, false, damage);
|
||||
_damageable.ChangeDamage((uid, damage), comp.Damage, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Standing;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Physics.Events;
|
||||
|
||||
namespace Content.Shared.Damage.Components;
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public sealed class RequireProjectileTargetSystem : EntitySystem
|
||||
{
|
||||
|
||||
@@ -85,9 +85,9 @@ public abstract class SharedGodmodeSystem : EntitySystem
|
||||
if (!Resolve(uid, ref godmode, false))
|
||||
return;
|
||||
|
||||
if (TryComp<DamageableComponent>(uid, out var damageable) && godmode.OldDamage != null)
|
||||
if (godmode.OldDamage != null)
|
||||
{
|
||||
_damageable.SetDamage(uid, damageable, godmode.OldDamage);
|
||||
_damageable.SetDamage(uid, godmode.OldDamage);
|
||||
}
|
||||
|
||||
RemComp<GodmodeComponent>(uid);
|
||||
|
||||
@@ -5,109 +5,108 @@ using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Systems;
|
||||
|
||||
namespace Content.Shared.Damage
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
public sealed class SlowOnDamageSystem : EntitySystem
|
||||
{
|
||||
public sealed class SlowOnDamageSystem : EntitySystem
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifierSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifierSystem = default!;
|
||||
base.Initialize();
|
||||
|
||||
public override void Initialize()
|
||||
SubscribeLocalEvent<SlowOnDamageComponent, DamageChangedEvent>(OnDamageChanged);
|
||||
SubscribeLocalEvent<SlowOnDamageComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, InventoryRelayedEvent<ModifySlowOnDamageSpeedEvent>>(OnModifySpeed);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ComponentStartup>(OnIgnoreStartup);
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ComponentShutdown>(OnIgnoreShutdown);
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ModifySlowOnDamageSpeedEvent>(OnIgnoreModifySpeed);
|
||||
}
|
||||
|
||||
private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
{
|
||||
if (!TryComp<DamageableComponent>(uid, out var damage))
|
||||
return;
|
||||
|
||||
if (damage.TotalDamage == FixedPoint2.Zero)
|
||||
return;
|
||||
|
||||
// Get closest threshold
|
||||
FixedPoint2 closest = FixedPoint2.Zero;
|
||||
var total = damage.TotalDamage;
|
||||
foreach (var thres in component.SpeedModifierThresholds)
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SlowOnDamageComponent, DamageChangedEvent>(OnDamageChanged);
|
||||
SubscribeLocalEvent<SlowOnDamageComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, InventoryRelayedEvent<ModifySlowOnDamageSpeedEvent>>(OnModifySpeed);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ComponentStartup>(OnIgnoreStartup);
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ComponentShutdown>(OnIgnoreShutdown);
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ModifySlowOnDamageSpeedEvent>(OnIgnoreModifySpeed);
|
||||
if (total >= thres.Key && thres.Key > closest)
|
||||
closest = thres.Key;
|
||||
}
|
||||
|
||||
private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
if (closest != FixedPoint2.Zero)
|
||||
{
|
||||
if (!TryComp<DamageableComponent>(uid, out var damage))
|
||||
return;
|
||||
var speed = component.SpeedModifierThresholds[closest];
|
||||
|
||||
if (damage.TotalDamage == FixedPoint2.Zero)
|
||||
return;
|
||||
|
||||
// Get closest threshold
|
||||
FixedPoint2 closest = FixedPoint2.Zero;
|
||||
var total = damage.TotalDamage;
|
||||
foreach (var thres in component.SpeedModifierThresholds)
|
||||
{
|
||||
if (total >= thres.Key && thres.Key > closest)
|
||||
closest = thres.Key;
|
||||
}
|
||||
|
||||
if (closest != FixedPoint2.Zero)
|
||||
{
|
||||
var speed = component.SpeedModifierThresholds[closest];
|
||||
|
||||
var ev = new ModifySlowOnDamageSpeedEvent(speed);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
args.ModifySpeed(ev.Speed, ev.Speed);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDamageChanged(EntityUid uid, SlowOnDamageComponent component, DamageChangedEvent args)
|
||||
{
|
||||
// We -could- only refresh if it crossed a threshold but that would kind of be a lot of duplicated
|
||||
// code and this isn't a super hot path anyway since basically only humans have this
|
||||
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid);
|
||||
}
|
||||
|
||||
private void OnModifySpeed(Entity<ClothingSlowOnDamageModifierComponent> ent, ref InventoryRelayedEvent<ModifySlowOnDamageSpeedEvent> args)
|
||||
{
|
||||
var dif = 1 - args.Args.Speed;
|
||||
if (dif <= 0)
|
||||
return;
|
||||
|
||||
// reduces the slowness modifier by the given coefficient
|
||||
args.Args.Speed += dif * ent.Comp.Modifier;
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
var msg = Loc.GetString("slow-on-damage-modifier-examine", ("mod", (1 - ent.Comp.Modifier) * 100));
|
||||
args.PushMarkup(msg);
|
||||
}
|
||||
|
||||
private void OnGotEquipped(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer);
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer);
|
||||
}
|
||||
|
||||
private void OnIgnoreStartup(Entity<IgnoreSlowOnDamageComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent);
|
||||
}
|
||||
|
||||
private void OnIgnoreShutdown(Entity<IgnoreSlowOnDamageComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent);
|
||||
}
|
||||
|
||||
private void OnIgnoreModifySpeed(Entity<IgnoreSlowOnDamageComponent> ent, ref ModifySlowOnDamageSpeedEvent args)
|
||||
{
|
||||
args.Speed = 1f;
|
||||
var ev = new ModifySlowOnDamageSpeedEvent(speed);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
args.ModifySpeed(ev.Speed, ev.Speed);
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct ModifySlowOnDamageSpeedEvent(float Speed) : IInventoryRelayEvent
|
||||
private void OnDamageChanged(EntityUid uid, SlowOnDamageComponent component, DamageChangedEvent args)
|
||||
{
|
||||
public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET;
|
||||
// We -could- only refresh if it crossed a threshold but that would kind of be a lot of duplicated
|
||||
// code and this isn't a super hot path anyway since basically only humans have this
|
||||
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(uid);
|
||||
}
|
||||
|
||||
private void OnModifySpeed(Entity<ClothingSlowOnDamageModifierComponent> ent, ref InventoryRelayedEvent<ModifySlowOnDamageSpeedEvent> args)
|
||||
{
|
||||
var dif = 1 - args.Args.Speed;
|
||||
if (dif <= 0)
|
||||
return;
|
||||
|
||||
// reduces the slowness modifier by the given coefficient
|
||||
args.Args.Speed += dif * ent.Comp.Modifier;
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
var msg = Loc.GetString("slow-on-damage-modifier-examine", ("mod", (1 - ent.Comp.Modifier) * 100));
|
||||
args.PushMarkup(msg);
|
||||
}
|
||||
|
||||
private void OnGotEquipped(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer);
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<ClothingSlowOnDamageModifierComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer);
|
||||
}
|
||||
|
||||
private void OnIgnoreStartup(Entity<IgnoreSlowOnDamageComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent);
|
||||
}
|
||||
|
||||
private void OnIgnoreShutdown(Entity<IgnoreSlowOnDamageComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent);
|
||||
}
|
||||
|
||||
private void OnIgnoreModifySpeed(Entity<IgnoreSlowOnDamageComponent> ent, ref ModifySlowOnDamageSpeedEvent args)
|
||||
{
|
||||
args.Speed = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct ModifySlowOnDamageSpeedEvent(float Speed) : IInventoryRelayEvent
|
||||
{
|
||||
public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ public abstract class SharedDeliverySystem : EntitySystem
|
||||
private bool TryUnlockDelivery(Entity<DeliveryComponent> ent, EntityUid user, bool rewardMoney = true, bool force = false)
|
||||
{
|
||||
// Check fingerprint access if there is a reader on the mail
|
||||
if (!force && TryComp<FingerprintReaderComponent>(ent, out var reader) && !_fingerprintReader.IsAllowed((ent, reader), user))
|
||||
if (!force && !_fingerprintReader.IsAllowed(ent.Owner, user, out _))
|
||||
return false;
|
||||
|
||||
var deliveryName = _nameModifier.GetBaseName(ent.Owner);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
namespace Content.Shared.Destructible;
|
||||
namespace Content.Shared.Destructible;
|
||||
|
||||
public abstract class SharedDestructibleSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Force entity to be destroyed and deleted.
|
||||
/// Force entity to be destroyed and deleted.
|
||||
/// </summary>
|
||||
public bool DestroyEntity(EntityUid owner)
|
||||
public bool DestroyEntity(Entity<MetaDataComponent?> owner)
|
||||
{
|
||||
var ev = new DestructionAttemptEvent();
|
||||
RaiseLocalEvent(owner, ev);
|
||||
@@ -15,12 +15,12 @@ public abstract class SharedDestructibleSystem : EntitySystem
|
||||
var eventArgs = new DestructionEventArgs();
|
||||
RaiseLocalEvent(owner, eventArgs);
|
||||
|
||||
QueueDel(owner);
|
||||
PredictedQueueDel(owner);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force entity to break.
|
||||
/// Force entity to break.
|
||||
/// </summary>
|
||||
public void BreakEntity(EntityUid owner)
|
||||
{
|
||||
@@ -30,7 +30,7 @@ public abstract class SharedDestructibleSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised before an entity is about to be destroyed and deleted
|
||||
/// Raised before an entity is about to be destroyed and deleted
|
||||
/// </summary>
|
||||
public sealed class DestructionAttemptEvent : CancellableEntityEventArgs
|
||||
{
|
||||
@@ -38,7 +38,7 @@ public sealed class DestructionAttemptEvent : CancellableEntityEventArgs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when entity is destroyed and about to be deleted.
|
||||
/// Raised when entity is destroyed and about to be deleted.
|
||||
/// </summary>
|
||||
public sealed class DestructionEventArgs : EntityEventArgs
|
||||
{
|
||||
@@ -46,7 +46,7 @@ public sealed class DestructionEventArgs : EntityEventArgs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when entity was heavy damage and about to break.
|
||||
/// Raised when entity was heavy damage and about to break.
|
||||
/// </summary>
|
||||
public sealed class BreakageEventArgs : EntityEventArgs
|
||||
{
|
||||
|
||||
12
Content.Shared/Destructible/ThresholdActs.cs
Normal file
12
Content.Shared/Destructible/ThresholdActs.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Destructible;
|
||||
|
||||
[Flags]
|
||||
[Serializable, NetSerializable]
|
||||
public enum ThresholdActs : byte
|
||||
{
|
||||
None = 0,
|
||||
Breakage = 1 << 0,
|
||||
Destruction = 1 << 1,
|
||||
}
|
||||
28
Content.Shared/Destructible/Triggers/AndTrigger.cs
Normal file
28
Content.Shared/Destructible/Triggers/AndTrigger.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Destructible.Thresholds.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// A trigger that will activate when all of its triggers have activated.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[DataDefinition]
|
||||
public sealed partial class AndTrigger : IThresholdTrigger
|
||||
{
|
||||
[DataField]
|
||||
public List<IThresholdTrigger> Triggers = new();
|
||||
|
||||
public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
|
||||
{
|
||||
foreach (var trigger in Triggers)
|
||||
{
|
||||
if (!trigger.Reached(damageable, system))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
33
Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs
Normal file
33
Content.Shared/Destructible/Triggers/DamageGroupTrigger.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Destructible.Thresholds.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// A trigger that will activate when the amount of damage received
|
||||
/// of the specified class is above the specified threshold.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[DataDefinition]
|
||||
public sealed partial class DamageGroupTrigger : IThresholdTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// The damage group to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<DamageGroupPrototype> DamageGroup = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of damage at which this threshold will trigger.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public FixedPoint2 Damage = default!;
|
||||
|
||||
public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
|
||||
{
|
||||
return damageable.Comp.DamagePerGroup[DamageGroup] >= Damage;
|
||||
}
|
||||
}
|
||||
25
Content.Shared/Destructible/Triggers/DamageTrigger.cs
Normal file
25
Content.Shared/Destructible/Triggers/DamageTrigger.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Destructible.Thresholds.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// A trigger that will activate when the total amount of damage received
|
||||
/// is above the specified threshold.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[DataDefinition]
|
||||
public sealed partial class DamageTrigger : IThresholdTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of damage at which this threshold will trigger.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public FixedPoint2 Damage = default!;
|
||||
|
||||
public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
|
||||
{
|
||||
return damageable.Comp.TotalDamage >= Damage;
|
||||
}
|
||||
}
|
||||
34
Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs
Normal file
34
Content.Shared/Destructible/Triggers/DamageTypeTrigger.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Destructible.Thresholds.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// A trigger that will activate when the amount of damage received
|
||||
/// of the specified type is above the specified threshold.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[DataDefinition]
|
||||
public sealed partial class DamageTypeTrigger : IThresholdTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// The damage type to check for.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<DamageTypePrototype> DamageType = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of damage at which this threshold will trigger.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public FixedPoint2 Damage = default!;
|
||||
|
||||
public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
|
||||
{
|
||||
return damageable.Comp.Damage.DamageDict.TryGetValue(DamageType, out var damageReceived) &&
|
||||
damageReceived >= Damage;
|
||||
}
|
||||
}
|
||||
27
Content.Shared/Destructible/Triggers/IThresholdTrigger.cs
Normal file
27
Content.Shared/Destructible/Triggers/IThresholdTrigger.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
namespace Content.Shared.Destructible.Thresholds.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// A condition for triggering a <see cref="DamageThreshold">.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// I decided against converting these into EntityEffectConditions for performance reasons
|
||||
/// (although I did not do any benchmarks, so it might be fine).
|
||||
/// Entity effects will raise a separate event for each entity and each condition, which can become a huge number
|
||||
/// for cases like nuke explosions or shuttle collisions where there are lots of DamageChangedEvents at once.
|
||||
/// IThresholdTriggers on the other hand are directly checked in a foreach loop without raising events.
|
||||
/// And there are only few of these conditions, so there is only a minor amount of code duplication.
|
||||
/// </remarks>
|
||||
public interface IThresholdTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if this trigger has been reached.
|
||||
/// </summary>
|
||||
/// <param name="damageable">The damageable component to check with.</param>
|
||||
/// <param name="system">
|
||||
/// An instance of <see cref="SharedDestructibleSystem"/> to pull dependencies from, if any.
|
||||
/// </param>
|
||||
/// <returns>true if this trigger has been reached, false otherwise.</returns>
|
||||
bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system);
|
||||
}
|
||||
28
Content.Shared/Destructible/Triggers/OrTrigger.cs
Normal file
28
Content.Shared/Destructible/Triggers/OrTrigger.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Destructible.Thresholds.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// A trigger that will activate when any of its triggers have activated.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[DataDefinition]
|
||||
public sealed partial class OrTrigger : IThresholdTrigger
|
||||
{
|
||||
[DataField]
|
||||
public List<IThresholdTrigger> Triggers = new();
|
||||
|
||||
public bool Reached(Entity<DamageableComponent> damageable, SharedDestructibleSystem system)
|
||||
{
|
||||
foreach (var trigger in Triggers)
|
||||
{
|
||||
if (trigger.Reached(damageable, system))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
@@ -325,4 +325,5 @@ public enum DoorVisualLayers : byte
|
||||
BaseUnlit,
|
||||
BaseBolted,
|
||||
BaseEmergencyAccess,
|
||||
BaseEmagging,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Linq;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -8,12 +7,6 @@ namespace Content.Shared.Drunk;
|
||||
public abstract class SharedDrunkSystem : EntitySystem
|
||||
{
|
||||
public static EntProtoId Drunk = "StatusEffectDrunk";
|
||||
public static EntProtoId Woozy = "StatusEffectWoozy";
|
||||
|
||||
/* I have no clue why this magic number was chosen, I copied it from slur system and needed it for the overlay
|
||||
If you have a more intelligent magic number be my guest to completely explode this value.
|
||||
There were no comments as to why this value was chosen three years ago. */
|
||||
public static float MagicNumber = 1100f;
|
||||
|
||||
[Dependency] protected readonly StatusEffectsSystem Status = default!;
|
||||
|
||||
|
||||
@@ -1,27 +1,6 @@
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Shared.Emoting;
|
||||
namespace Content.Shared.Emoting;
|
||||
|
||||
public sealed class EmoteAttemptEvent(EntityUid uid) : CancellableEntityEventArgs
|
||||
{
|
||||
public EntityUid Uid { get; } = uid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An event raised just before an emote is performed, providing systems with an opportunity to cancel the emote's performance.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public sealed class BeforeEmoteEvent(EntityUid source, EmotePrototype emote)
|
||||
: CancellableEntityEventArgs, IInventoryRelayEvent
|
||||
{
|
||||
public readonly EntityUid Source = source;
|
||||
public readonly EmotePrototype Emote = emote;
|
||||
|
||||
/// <summary>
|
||||
/// The equipment that is blocking emoting. Should only be non-null if the event was canceled.
|
||||
/// </summary>
|
||||
public EntityUid? Blocker = null;
|
||||
|
||||
public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Body;
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class BreathingCondition : EntityConditionBase<BreathingCondition>
|
||||
{
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
|
||||
Loc.GetString("entity-condition-guidebook-breathing", ("isBreathing", !Inverted));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Body;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity's hunger is within a specified minimum and maximum.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class TotalHungerEntityConditionSystem : EntityConditionSystem<HungerComponent, HungerCondition>
|
||||
{
|
||||
[Dependency] private readonly HungerSystem _hunger = default!;
|
||||
|
||||
protected override void Condition(Entity<HungerComponent> entity, ref EntityConditionEvent<HungerCondition> args)
|
||||
{
|
||||
var total = _hunger.GetHunger(entity.Comp);
|
||||
args.Result = total >= args.Condition.Min && total <= args.Condition.Max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class HungerCondition : EntityConditionBase<HungerCondition>
|
||||
{
|
||||
[DataField]
|
||||
public float Min;
|
||||
|
||||
[DataField]
|
||||
public float Max = float.PositiveInfinity;
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
|
||||
Loc.GetString("entity-condition-guidebook-total-hunger", ("max", float.IsPositiveInfinity(Max) ? int.MaxValue : Max), ("min", Min));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Body.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Body;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity is using internals. False if they are not or cannot use internals.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class InternalsOnEntityConditionSystem : EntityConditionSystem<InternalsComponent, InternalsCondition>
|
||||
{
|
||||
protected override void Condition(Entity<InternalsComponent> entity, ref EntityConditionEvent<InternalsCondition> args)
|
||||
{
|
||||
args.Result = entity.Comp.GasTankEntity != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class InternalsCondition : EntityConditionBase<InternalsCondition>
|
||||
{
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
|
||||
Loc.GetString("entity-condition-guidebook-internals", ("usingInternals", !Inverted));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Body;
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class MetabolizerTypeCondition : EntityConditionBase<MetabolizerTypeCondition>
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<MetabolizerTypePrototype>[] Type = default!;
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
|
||||
{
|
||||
var typeList = new List<string>();
|
||||
|
||||
foreach (var type in Type)
|
||||
{
|
||||
if (!prototype.Resolve(type, out var proto))
|
||||
continue;
|
||||
|
||||
typeList.Add(proto.LocalizedName);
|
||||
}
|
||||
|
||||
var names = ContentLocalizationManager.FormatListToOr(typeList);
|
||||
|
||||
return Loc.GetString("entity-condition-guidebook-organ-type",
|
||||
("name", names),
|
||||
("shouldhave", !Inverted));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Body;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity's current mob state matches the condition's specified mob state.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class MobStateEntityConditionSystem : EntityConditionSystem<MobStateComponent, MobStateCondition>
|
||||
{
|
||||
protected override void Condition(Entity<MobStateComponent> entity, ref EntityConditionEvent<MobStateCondition> args)
|
||||
{
|
||||
if (entity.Comp.CurrentState == args.Condition.Mobstate)
|
||||
args.Result = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class MobStateCondition : EntityConditionBase<MobStateCondition>
|
||||
{
|
||||
[DataField]
|
||||
public MobState Mobstate = MobState.Alive;
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
|
||||
Loc.GetString("entity-condition-guidebook-mob-state-condition", ("state", Mobstate));
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity can take damage and if its damage of a given damage group is within a specified minimum and maximum.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class DamageGroupEntityConditionSystem : EntityConditionSystem<DamageableComponent, DamageGroupCondition>
|
||||
{
|
||||
protected override void Condition(Entity<DamageableComponent> entity, ref EntityConditionEvent<DamageGroupCondition> args)
|
||||
{
|
||||
var value = entity.Comp.DamagePerGroup[args.Condition.DamageGroup];
|
||||
args.Result = value >= args.Condition.Min && value <= args.Condition.Max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class DamageGroupCondition : EntityConditionBase<DamageGroupCondition>
|
||||
{
|
||||
[DataField]
|
||||
public FixedPoint2 Max = FixedPoint2.MaxValue;
|
||||
|
||||
[DataField]
|
||||
public FixedPoint2 Min = FixedPoint2.Zero;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<DamageGroupPrototype> DamageGroup;
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
|
||||
Loc.GetString("entity-condition-guidebook-group-damage",
|
||||
("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()),
|
||||
("min", Min.Float()),
|
||||
("type", prototype.Index(DamageGroup).LocalizedName));
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity can take damage and if its damage of a given damage type is within a specified minimum and maximum.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class DamageTypeEntityConditionSystem : EntityConditionSystem<DamageableComponent, DamageTypeCondition>
|
||||
{
|
||||
protected override void Condition(Entity<DamageableComponent> entity, ref EntityConditionEvent<DamageTypeCondition> args)
|
||||
{
|
||||
var value = entity.Comp.Damage.DamageDict.GetValueOrDefault(args.Condition.DamageType);
|
||||
args.Result = value >= args.Condition.Min && value <= args.Condition.Max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class DamageTypeCondition : EntityConditionBase<DamageTypeCondition>
|
||||
{
|
||||
[DataField]
|
||||
public FixedPoint2 Max = FixedPoint2.MaxValue;
|
||||
|
||||
[DataField]
|
||||
public FixedPoint2 Min = FixedPoint2.Zero;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<DamageTypePrototype> DamageType;
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
|
||||
Loc.GetString("entity-condition-guidebook-type-damage",
|
||||
("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()),
|
||||
("min", Min.Float()),
|
||||
("type", prototype.Index(DamageType).LocalizedName));
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity has any of the specified jobs. False if the entity has no mind, none of the specified jobs, or is jobless.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class HasJobEntityConditionSystem : EntityConditionSystem<MindContainerComponent, JobCondition>
|
||||
{
|
||||
protected override void Condition(Entity<MindContainerComponent> entity, ref EntityConditionEvent<JobCondition> args)
|
||||
{
|
||||
// We need a mind in our mind container...
|
||||
if (!TryComp<MindComponent>(entity.Comp.Mind, out var mind))
|
||||
return;
|
||||
|
||||
foreach (var roleId in mind.MindRoleContainer.ContainedEntities)
|
||||
{
|
||||
if (!HasComp<JobRoleComponent>(roleId))
|
||||
continue;
|
||||
|
||||
if (!TryComp<MindRoleComponent>(roleId, out var mindRole))
|
||||
{
|
||||
Log.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mindRole.JobPrototype == null)
|
||||
{
|
||||
Log.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!args.Condition.Jobs.Contains(mindRole.JobPrototype.Value))
|
||||
continue;
|
||||
|
||||
args.Result = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class JobCondition : EntityConditionBase<JobCondition>
|
||||
{
|
||||
[DataField(required: true)] public List<ProtoId<JobPrototype>> Jobs = [];
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
|
||||
{
|
||||
var localizedNames = Jobs.Select(jobId => prototype.Index(jobId).LocalizedName).ToList();
|
||||
return Loc.GetString("entity-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this solution entity has an amount of reagent in it within a specified minimum and maximum.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class ReagentThresholdEntityConditionSystem : EntityConditionSystem<SolutionComponent, ReagentCondition>
|
||||
{
|
||||
protected override void Condition(Entity<SolutionComponent> entity, ref EntityConditionEvent<ReagentCondition> args)
|
||||
{
|
||||
var quant = entity.Comp.Solution.GetTotalPrototypeQuantity(args.Condition.Reagent);
|
||||
|
||||
args.Result = quant >= args.Condition.Min && quant <= args.Condition.Max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class ReagentCondition : EntityConditionBase<ReagentCondition>
|
||||
{
|
||||
[DataField]
|
||||
public FixedPoint2 Min = FixedPoint2.Zero;
|
||||
|
||||
[DataField]
|
||||
public FixedPoint2 Max = FixedPoint2.MaxValue;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ProtoId<ReagentPrototype> Reagent;
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
|
||||
{
|
||||
if (!prototype.Resolve(Reagent, out var reagentProto))
|
||||
return String.Empty;
|
||||
|
||||
return Loc.GetString("entity-condition-guidebook-reagent-threshold",
|
||||
("reagent", reagentProto.LocalizedName),
|
||||
("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()),
|
||||
("min", Min.Float()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Tags;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity has all the listed tags.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class HasAllTagsEntityConditionSystem : EntityConditionSystem<TagComponent, AllTagsCondition>
|
||||
{
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
|
||||
protected override void Condition(Entity<TagComponent> entity, ref EntityConditionEvent<AllTagsCondition> args)
|
||||
{
|
||||
args.Result = _tag.HasAllTags(entity.Comp, args.Condition.Tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class AllTagsCondition : EntityConditionBase<AllTagsCondition>
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<TagPrototype>[] Tags = [];
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
|
||||
{
|
||||
var tagList = new List<string>();
|
||||
|
||||
foreach (var type in Tags)
|
||||
{
|
||||
if (!prototype.Resolve(type, out var proto))
|
||||
continue;
|
||||
|
||||
tagList.Add(proto.ID);
|
||||
}
|
||||
|
||||
var names = ContentLocalizationManager.FormatList(tagList);
|
||||
|
||||
return Loc.GetString("entity-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Tags;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity have any of the listed tags.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class HasAnyTagEntityConditionSystem : EntityConditionSystem<TagComponent, AnyTagCondition>
|
||||
{
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
|
||||
protected override void Condition(Entity<TagComponent> entity, ref EntityConditionEvent<AnyTagCondition> args)
|
||||
{
|
||||
args.Result = _tag.HasAnyTag(entity.Comp, args.Condition.Tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class AnyTagCondition : EntityConditionBase<AnyTagCondition>
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<TagPrototype>[] Tags = [];
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
|
||||
{
|
||||
var tagList = new List<string>();
|
||||
|
||||
foreach (var type in Tags)
|
||||
{
|
||||
if (!prototype.Resolve(type, out var proto))
|
||||
continue;
|
||||
|
||||
tagList.Add(proto.ID);
|
||||
}
|
||||
|
||||
var names = ContentLocalizationManager.FormatListToOr(tagList);
|
||||
|
||||
return Loc.GetString("entity-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityConditions.Conditions.Tags;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this entity has the listed tag.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
|
||||
public sealed partial class HasTagEntityConditionSystem : EntityConditionSystem<TagComponent, TagCondition>
|
||||
{
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
|
||||
protected override void Condition(Entity<TagComponent> entity, ref EntityConditionEvent<TagCondition> args)
|
||||
{
|
||||
args.Result = _tag.HasTag(entity.Comp, args.Condition.Tag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="EntityCondition"/>
|
||||
public sealed partial class TagCondition : EntityConditionBase<TagCondition>
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<TagPrototype> Tag;
|
||||
|
||||
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
|
||||
Loc.GetString("entity-condition-guidebook-has-tag", ("tag", Tag), ("invert", Inverted));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user