Merge remote-tracking branch 'upstream/stable' into ed-08-09-2025-upstream-sync
# Conflicts: # .github/CODEOWNERS # Content.IntegrationTests/Tests/CargoTest.cs # Content.Server/Chat/Systems/ChatSystem.cs # Content.Shared/Chat/SharedChatSystem.cs # Content.Shared/Lock/LockSystem.cs # Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs # Content.Shared/Storage/Components/EntityStorageComponent.cs # Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml # Resources/Prototypes/game_presets.yml
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
[RegisterComponent, Access(typeof(SharedAdminFrozenSystem))]
|
||||
[RegisterComponent, Access(typeof(AdminFrozenSystem))]
|
||||
[NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class AdminFrozenComponent : Component
|
||||
{
|
||||
|
||||
@@ -12,15 +12,13 @@ using Content.Shared.Throwing;
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
// TODO deduplicate with BlockMovementComponent
|
||||
public abstract class SharedAdminFrozenSystem : EntitySystem
|
||||
public sealed class AdminFrozenSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
[Dependency] private readonly PullingSystem _pulling = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AdminFrozenComponent, UseAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, PickupAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, ThrowAttemptEvent>(OnAttempt);
|
||||
@@ -35,6 +33,16 @@ public abstract class SharedAdminFrozenSystem : EntitySystem
|
||||
SubscribeLocalEvent<AdminFrozenComponent, SpeakAttemptEvent>(OnSpeakAttempt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Freezes and mutes the given entity.
|
||||
/// </summary>
|
||||
public void FreezeAndMute(EntityUid uid)
|
||||
{
|
||||
var comp = EnsureComp<AdminFrozenComponent>(uid);
|
||||
comp.Muted = true;
|
||||
Dirty(uid, comp);
|
||||
}
|
||||
|
||||
private void OnInteractAttempt(Entity<AdminFrozenComponent> ent, ref InteractionAttemptEvent args)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
99
Content.Shared/Atmos/Components/FlammableComponent.cs
Normal file
99
Content.Shared/Atmos/Components/FlammableComponent.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Damage;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Atmos.Components
|
||||
{
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class FlammableComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public bool Resisting;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool OnFire;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float FireStacks;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float MaximumFireStacks = 10f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float MinimumFireStacks = -10f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public string FlammableFixtureID = "flammable";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float MinIgnitionTemperature = 373.15f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool FireSpread { get; private set; } = false;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool CanResistFire { get; private set; } = false;
|
||||
|
||||
[DataField(required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DamageSpecifier Damage = new(); // Empty by default, we don't want any funny NREs.
|
||||
|
||||
/// <summary>
|
||||
/// Used for the fixture created to handle passing firestacks when two flammable objects collide.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public IPhysShape FlammableCollisionShape = new PhysShapeCircle(0.35f);
|
||||
|
||||
/// <summary>
|
||||
/// Should the component be set on fire by interactions with isHot entities
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool AlwaysCombustible = false;
|
||||
|
||||
/// <summary>
|
||||
/// Can the component anyhow lose its FireStacks?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool CanExtinguish = true;
|
||||
|
||||
/// <summary>
|
||||
/// How many firestacks should be applied to component when being set on fire?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public float FirestacksOnIgnite = 2.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how quickly the object will fade out. With positive values, the object will flare up instead of going out.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float FirestackFade = -0.1f;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<AlertPrototype> FireAlert = "Fire";
|
||||
|
||||
/// <summary>
|
||||
/// CrystallEdge fireplace fuel
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float CP14FireplaceFuel = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// the value is cached to check if it has changed
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool OnFireOld = false;
|
||||
}
|
||||
}
|
||||
@@ -198,12 +198,6 @@ public sealed partial class BloodstreamComponent : Component
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? TemporarySolution;
|
||||
|
||||
/// <summary>
|
||||
/// Variable that stores the amount of status time added by having a low blood level.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan StatusTime;
|
||||
|
||||
/// <summary>
|
||||
/// Alert to show when bleeding.
|
||||
/// </summary>
|
||||
|
||||
9
Content.Shared/Body/Events/BleedModifierEvent.cs
Normal file
9
Content.Shared/Body/Events/BleedModifierEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Content.Shared.Body.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity before they bleed to modify the amount.
|
||||
/// </summary>
|
||||
/// <param name="BleedAmount">The amount of blood the entity will lose.</param>
|
||||
/// <param name="BleedReductionAmount">The amount of bleed reduction that will happen.</param>
|
||||
[ByRefEvent]
|
||||
public record struct BleedModifierEvent(float BleedAmount, float BleedReductionAmount);
|
||||
@@ -6,7 +6,6 @@ using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Drunk;
|
||||
using Content.Shared.EntityEffects.Effects;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids;
|
||||
@@ -16,7 +15,7 @@ using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -27,17 +26,18 @@ namespace Content.Shared.Body.Systems;
|
||||
|
||||
public abstract class SharedBloodstreamSystem : EntitySystem
|
||||
{
|
||||
public static readonly EntProtoId Bloodloss = "StatusEffectBloodloss";
|
||||
|
||||
[Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedPuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _status = default!;
|
||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly SharedDrunkSystem _drunkSystem = default!;
|
||||
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -81,10 +81,14 @@ public abstract class SharedBloodstreamSystem : EntitySystem
|
||||
// as well as stop their bleeding to a certain extent.
|
||||
if (bloodstream.BleedAmount > 0)
|
||||
{
|
||||
var ev = new BleedModifierEvent(bloodstream.BleedAmount, bloodstream.BleedReductionAmount);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
// Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
|
||||
TryModifyBloodLevel((uid, bloodstream), -bloodstream.BleedAmount);
|
||||
TryModifyBloodLevel((uid, bloodstream), -ev.BleedAmount);
|
||||
|
||||
// Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
|
||||
TryModifyBleedAmount((uid, bloodstream), -bloodstream.BleedReductionAmount);
|
||||
TryModifyBleedAmount((uid, bloodstream), -ev.BleedReductionAmount);
|
||||
}
|
||||
|
||||
// deal bloodloss damage if their blood level is below a threshold.
|
||||
@@ -100,15 +104,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
|
||||
// Apply dizziness as a symptom of bloodloss.
|
||||
// The effect is applied in a way that it will never be cleared without being healthy.
|
||||
// Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
|
||||
_drunkSystem.TryApplyDrunkenness(
|
||||
uid,
|
||||
(float)bloodstream.AdjustedUpdateInterval.TotalSeconds * 2,
|
||||
applySlur: false);
|
||||
_stutteringSystem.DoStutter(uid, bloodstream.AdjustedUpdateInterval * 2, refresh: false);
|
||||
|
||||
// storing the drunk and stutter time so we can remove it independently from other effects additions
|
||||
bloodstream.StatusTime += bloodstream.AdjustedUpdateInterval * 2;
|
||||
DirtyField(uid, bloodstream, nameof(BloodstreamComponent.StatusTime));
|
||||
_status.TrySetStatusEffectDuration(uid, Bloodloss);
|
||||
}
|
||||
else if (!_mobStateSystem.IsDead(uid))
|
||||
{
|
||||
@@ -118,12 +114,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
|
||||
bloodstream.BloodlossHealDamage * bloodPercentage,
|
||||
ignoreResistances: true, interruptsDoAfters: false);
|
||||
|
||||
// Remove the drunk effect when healthy. Should only remove the amount of drunk and stutter added by low blood level
|
||||
_drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime.TotalSeconds);
|
||||
_stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime.TotalSeconds);
|
||||
// Reset the drunk and stutter time to zero
|
||||
bloodstream.StatusTime = TimeSpan.Zero;
|
||||
DirtyField(uid, bloodstream, nameof(BloodstreamComponent.StatusTime));
|
||||
_status.TryRemoveStatusEffect(uid, Bloodloss);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Database;
|
||||
@@ -13,7 +12,6 @@ using Content.Shared.Movement.Pulling.Events;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Pulling.Events;
|
||||
using Content.Shared.Rotation;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Stunnable;
|
||||
@@ -453,9 +451,9 @@ public abstract partial class SharedBuckleSystem
|
||||
private void Unbuckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
|
||||
{
|
||||
if (user == buckle.Owner)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}");
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} unbuckled themselves from {ToPrettyString(strap):strap}");
|
||||
else if (user != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}");
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} unbuckled {ToPrettyString(buckle):target} from {ToPrettyString(strap):strap}");
|
||||
|
||||
_audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);
|
||||
|
||||
|
||||
@@ -56,4 +56,12 @@ public sealed partial class CCVars
|
||||
[CVarControl(AdminFlags.VarEdit)]
|
||||
public static readonly CVarDef<float> MovementPushMassCap =
|
||||
CVarDef.Create("movement.push_mass_cap", 1.75f, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Is crawling enabled
|
||||
/// </summary>
|
||||
[CVarControl(AdminFlags.VarEdit)]
|
||||
public static readonly CVarDef<bool> MovementCrawling =
|
||||
CVarDef.Create("movement.crawling", true, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using Content.Shared.DoAfter;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Changeling.Devour;
|
||||
namespace Content.Shared.Changeling;
|
||||
|
||||
/// <summary>
|
||||
/// Action event for Devour, someone has initiated a devour on someone, begin to windup.
|
||||
@@ -2,7 +2,7 @@
|
||||
using Content.Shared.DoAfter;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Changeling.Transform;
|
||||
namespace Content.Shared.Changeling;
|
||||
|
||||
/// <summary>
|
||||
/// Action event for opening the changeling transformation radial menu.
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Changeling.Systems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
@@ -7,7 +8,7 @@ using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Changeling.Devour;
|
||||
namespace Content.Shared.Changeling.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Component responsible for Changelings Devour attack. Including the amount of damage
|
||||
@@ -2,13 +2,13 @@ using Content.Shared.Cloning;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Changeling;
|
||||
namespace Content.Shared.Changeling.Components;
|
||||
|
||||
/// <summary>
|
||||
/// The storage component for Changelings, it handles the link between a changeling and its consumed identities
|
||||
/// that exist on a paused map.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
|
||||
public sealed partial class ChangelingIdentityComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
@@ -29,6 +29,7 @@ public sealed partial class ChangelingIdentityComponent : Component
|
||||
/// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their
|
||||
/// respective systems.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<CloningSettingsPrototype> IdentityCloningSettings = "ChangelingCloningSettings";
|
||||
|
||||
public override bool SendOnlyToOwner => true;
|
||||
@@ -1,7 +1,7 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.Changeling;
|
||||
namespace Content.Shared.Changeling.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Marker component for cloned identities devoured by a changeling.
|
||||
@@ -1,9 +1,10 @@
|
||||
using Content.Shared.Changeling.Systems;
|
||||
using Content.Shared.Cloning;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Changeling.Transform;
|
||||
namespace Content.Shared.Changeling.Components;
|
||||
|
||||
/// <summary>
|
||||
/// The component containing information about Changelings Transformation action
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Administration.Logs;
|
||||
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.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
@@ -19,7 +20,7 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Changeling.Devour;
|
||||
namespace Content.Shared.Changeling.Systems;
|
||||
|
||||
public sealed class ChangelingDevourSystem : EntitySystem
|
||||
{
|
||||
@@ -31,7 +32,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly ChangelingIdentitySystem _changelingIdentitySystem = default!;
|
||||
[Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
@@ -0,0 +1,21 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Changeling.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Send when a player selects an intentity to transform into in the radial menu.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The uid of the cloned identity.
|
||||
/// </summary>
|
||||
public readonly NetEntity TargetIdentity = targetIdentity;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum ChangelingTransformUiKey : byte
|
||||
{
|
||||
Key,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
@@ -10,7 +11,7 @@ using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Changeling.Transform;
|
||||
namespace Content.Shared.Changeling.Systems;
|
||||
|
||||
public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
{
|
||||
@@ -43,7 +44,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
_actionsSystem.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction);
|
||||
|
||||
var userInterfaceComp = EnsureComp<UserInterfaceComponent>(ent);
|
||||
_uiSystem.SetUi((ent, userInterfaceComp), TransformUI.Key, new InterfaceData(ChangelingBuiXmlGeneratedName));
|
||||
_uiSystem.SetUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, new InterfaceData(ChangelingBuiXmlGeneratedName));
|
||||
}
|
||||
|
||||
private void OnShutdown(Entity<ChangelingTransformComponent> ent, ref ComponentShutdown args)
|
||||
@@ -63,18 +64,9 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
if (!TryComp<ChangelingIdentityComponent>(ent, out var userIdentity))
|
||||
return;
|
||||
|
||||
if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), TransformUI.Key, args.Performer))
|
||||
if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer))
|
||||
{
|
||||
_uiSystem.OpenUi((ent, userInterfaceComp), TransformUI.Key, args.Performer);
|
||||
|
||||
var identityData = new List<NetEntity>();
|
||||
|
||||
foreach (var consumedIdentity in userIdentity.ConsumedIdentities)
|
||||
{
|
||||
identityData.Add(GetNetEntity(consumedIdentity));
|
||||
}
|
||||
|
||||
_uiSystem.SetUiState((ent, userInterfaceComp), TransformUI.Key, new ChangelingTransformBoundUserInterfaceState(identityData));
|
||||
_uiSystem.OpenUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer);
|
||||
} //TODO: Can add a Else here with TransformInto and CloseUI to make a quick switch,
|
||||
// issue right now is that Radials cover the Action buttons so clicking the action closes the UI (due to clicking off a radial causing it to close, even with UI)
|
||||
// but pressing the number does.
|
||||
@@ -102,12 +94,12 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
if (_net.IsServer)
|
||||
ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity;
|
||||
|
||||
if(TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
|
||||
if (TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) ");
|
||||
else
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\"");
|
||||
|
||||
var result = _doAfterSystem.TryStartDoAfter(new DoAfterArgs(
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(
|
||||
EntityManager,
|
||||
ent,
|
||||
ent.Comp.TransformWindup,
|
||||
@@ -126,7 +118,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
private void OnTransformSelected(Entity<ChangelingTransformComponent> ent,
|
||||
ref ChangelingTransformIdentitySelectMessage args)
|
||||
{
|
||||
_uiSystem.CloseUi(ent.Owner, TransformUI.Key, ent);
|
||||
_uiSystem.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent);
|
||||
|
||||
if (!TryGetEntity(args.TargetIdentity, out var targetIdentity))
|
||||
return;
|
||||
@@ -162,8 +154,8 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
|
||||
|
||||
_humanoidAppearanceSystem.CloneAppearance(targetIdentity, args.User);
|
||||
_cloningSystem.CloneComponents(targetIdentity, args.User, settings);
|
||||
|
||||
if(TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
|
||||
|
||||
if (TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
|
||||
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})");
|
||||
else
|
||||
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\"");
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Changeling.Components;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
@@ -9,9 +9,9 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Changeling;
|
||||
namespace Content.Shared.Changeling.Systems;
|
||||
|
||||
public sealed class ChangelingIdentitySystem : EntitySystem
|
||||
public abstract class SharedChangelingIdentitySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
@@ -31,22 +31,19 @@ public sealed class ChangelingIdentitySystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, MindAddedMessage>(OnMindAdded);
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, MindRemovedMessage>(OnMindRemoved);
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<ChangelingIdentityComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<ChangelingStoredIdentityComponent, ComponentRemove>(OnStoredRemove);
|
||||
}
|
||||
|
||||
private void OnMindAdded(Entity<ChangelingIdentityComponent> ent, ref MindAddedMessage args)
|
||||
private void OnPlayerAttached(Entity<ChangelingIdentityComponent> ent, ref PlayerAttachedEvent args)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(args.Container.Owner, out var actor))
|
||||
return;
|
||||
|
||||
HandOverPvsOverride(actor.PlayerSession, ent.Comp);
|
||||
HandOverPvsOverride(ent, args.Player);
|
||||
}
|
||||
|
||||
private void OnMindRemoved(Entity<ChangelingIdentityComponent> ent, ref MindRemovedMessage args)
|
||||
private void OnPlayerDetached(Entity<ChangelingIdentityComponent> ent, ref PlayerDetachedEvent args)
|
||||
{
|
||||
CleanupPvsOverride(ent, args.Container.Owner);
|
||||
CleanupPvsOverride(ent, args.Player);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<ChangelingIdentityComponent> ent, ref MapInitEvent args)
|
||||
@@ -58,7 +55,8 @@ public sealed class ChangelingIdentitySystem : EntitySystem
|
||||
|
||||
private void OnShutdown(Entity<ChangelingIdentityComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
CleanupPvsOverride(ent, ent.Owner);
|
||||
if (TryComp<ActorComponent>(ent, out var actor))
|
||||
CleanupPvsOverride(ent, actor.PlayerSession);
|
||||
CleanupChangelingNullspaceIdentities(ent);
|
||||
}
|
||||
|
||||
@@ -106,66 +104,63 @@ public sealed class ChangelingIdentitySystem : EntitySystem
|
||||
// Movercontrollers and mob collisions are currently being calculated even for paused entities.
|
||||
// Spawning all of them in the same spot causes severe performance problems.
|
||||
// Cryopods and Polymorph have the same problem.
|
||||
var mob = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value));
|
||||
var clone = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value));
|
||||
|
||||
var storedIdentity = EnsureComp<ChangelingStoredIdentityComponent>(mob);
|
||||
var storedIdentity = EnsureComp<ChangelingStoredIdentityComponent>(clone);
|
||||
storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed
|
||||
|
||||
if (TryComp<ActorComponent>(target, out var actor))
|
||||
storedIdentity.OriginalSession = actor.PlayerSession;
|
||||
|
||||
_humanoidSystem.CloneAppearance(target, mob);
|
||||
_cloningSystem.CloneComponents(target, mob, settings);
|
||||
_humanoidSystem.CloneAppearance(target, clone);
|
||||
_cloningSystem.CloneComponents(target, clone, settings);
|
||||
|
||||
var targetName = _nameMod.GetBaseName(target);
|
||||
_metaSystem.SetEntityName(mob, targetName);
|
||||
ent.Comp.ConsumedIdentities.Add(mob);
|
||||
_metaSystem.SetEntityName(clone, targetName);
|
||||
ent.Comp.ConsumedIdentities.Add(clone);
|
||||
|
||||
Dirty(ent);
|
||||
HandlePvsOverride(ent, mob);
|
||||
HandlePvsOverride(ent, clone);
|
||||
|
||||
return mob;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple helper to add a PVS override to a Nullspace Identity
|
||||
/// Simple helper to add a PVS override to a nullspace identity.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="target"></param>
|
||||
private void HandlePvsOverride(EntityUid uid, EntityUid target)
|
||||
/// <param name="uid">The actor that should get the override.</param>
|
||||
/// <param name="identity">The identity stored in nullspace.</param>
|
||||
private void HandlePvsOverride(EntityUid uid, EntityUid identity)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(uid, out var actor))
|
||||
return;
|
||||
|
||||
_pvsOverrideSystem.AddSessionOverride(target, actor.PlayerSession);
|
||||
_pvsOverrideSystem.AddSessionOverride(identity, actor.PlayerSession);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup all Pvs Overrides for the owner of the ChangelingIdentity
|
||||
/// Cleanup all PVS overrides for the owner of the ChangelingIdentity
|
||||
/// </summary>
|
||||
/// <param name="ent">the Changeling itself</param>
|
||||
/// <param name="entityUid">Who specifically to cleanup from, usually just the same owner, but in the case of a mindswap we want to clean up the victim</param>
|
||||
private void CleanupPvsOverride(Entity<ChangelingIdentityComponent> ent, EntityUid entityUid)
|
||||
/// <param name="ent">The changeling storing the identities.</param>
|
||||
/// <param name="entityUid"The session you wish to remove the overrides from.</param>
|
||||
private void CleanupPvsOverride(Entity<ChangelingIdentityComponent> ent, ICommonSession session)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(entityUid, out var actor))
|
||||
return;
|
||||
|
||||
foreach (var identity in ent.Comp.ConsumedIdentities)
|
||||
{
|
||||
_pvsOverrideSystem.RemoveSessionOverride(identity, actor.PlayerSession);
|
||||
_pvsOverrideSystem.RemoveSessionOverride(identity, session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inform another Session of the entities stored for Transformation
|
||||
/// Inform another session of the entities stored for transformation.
|
||||
/// </summary>
|
||||
/// <param name="session">The Session you wish to inform</param>
|
||||
/// <param name="comp">The Target storage of identities</param>
|
||||
public void HandOverPvsOverride(ICommonSession session, ChangelingIdentityComponent comp)
|
||||
/// <param name="ent">The changeling storing the identities.</param>
|
||||
/// <param name="session">The session you wish to inform.</param>
|
||||
public void HandOverPvsOverride(Entity<ChangelingIdentityComponent> ent, ICommonSession session)
|
||||
{
|
||||
foreach (var entity in comp.ConsumedIdentities)
|
||||
foreach (var identity in ent.Comp.ConsumedIdentities)
|
||||
{
|
||||
_pvsOverrideSystem.AddSessionOverride(entity, session);
|
||||
_pvsOverrideSystem.AddSessionOverride(identity, session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Changeling.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Send when a player selects an intentity to transform into in the radial menu.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The uid of the cloned identity.
|
||||
/// </summary>
|
||||
public readonly NetEntity TargetIdentity = targetIdentity;
|
||||
}
|
||||
|
||||
// TODO: Replace with component states.
|
||||
// We are already networking the ChangelingIdentityComponent, which contains all this information,
|
||||
// so we can just read it from them from the component and update the UI in an AfterAuotHandleState subscription.
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ChangelingTransformBoundUserInterfaceState(List<NetEntity> identities) : BoundUserInterfaceState
|
||||
{
|
||||
/// <summary>
|
||||
/// The uids of the cloned identities.
|
||||
/// </summary>
|
||||
public readonly List<NetEntity> Identites = identities;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum TransformUI : byte
|
||||
{
|
||||
Key,
|
||||
}
|
||||
@@ -16,7 +16,7 @@ public sealed partial class LimitedChargesComponent : Component
|
||||
/// <summary>
|
||||
/// The max charges this action has.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField, Access(Other = AccessPermissions.Read)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public int MaxCharges = 3;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -94,6 +94,12 @@ public abstract class SharedChargesSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Adds the specified charges. Does not reset the accumulator.
|
||||
/// </summary>
|
||||
/// <param name="action">
|
||||
/// The action to add charges to. If it doesn't have <see cref="LimitedChargesComponent"/>, it will be added.
|
||||
/// </param>
|
||||
/// <param name="addCharges">
|
||||
/// The number of charges to add. Can be negative. Resulting charge count is clamped to [0, MaxCharges].
|
||||
/// </param>
|
||||
public void AddCharges(Entity<LimitedChargesComponent?, AutoRechargeComponent?> action, int addCharges)
|
||||
{
|
||||
if (addCharges == 0)
|
||||
@@ -170,9 +176,21 @@ public abstract class SharedChargesSystem : EntitySystem
|
||||
Dirty(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the number of charges an action has.
|
||||
/// </summary>
|
||||
/// <param name="action">The action in question</param>
|
||||
/// <param name="value">
|
||||
/// The number of charges. Clamped to [0, MaxCharges].
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This method doesn't implicitly add <see cref="LimitedChargesComponent"/>
|
||||
/// unlike some other methods in this system.
|
||||
/// </remarks>
|
||||
public void SetCharges(Entity<LimitedChargesComponent?> action, int value)
|
||||
{
|
||||
action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
|
||||
if (!Resolve(action, ref action.Comp))
|
||||
return;
|
||||
|
||||
var adjusted = Math.Clamp(value, 0, action.Comp.MaxCharges);
|
||||
|
||||
@@ -186,6 +204,31 @@ public abstract class SharedChargesSystem : EntitySystem
|
||||
Dirty(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum charges of a given action.
|
||||
/// </summary>
|
||||
/// <param name="action">The action being modified.</param>
|
||||
/// <param name="value">The new maximum charges of the action. Clamped to zero.</param>
|
||||
/// <remarks>
|
||||
/// Does not change the current charge count, or adjust the
|
||||
/// accumulator for auto-recharge. It also doesn't implicitly add
|
||||
/// <see cref="LimitedChargesComponent"/> unlike some other methods
|
||||
/// in this system.
|
||||
/// </remarks>
|
||||
public void SetMaxCharges(Entity<LimitedChargesComponent?> action, int value)
|
||||
{
|
||||
if (!Resolve(action, ref action.Comp))
|
||||
return;
|
||||
|
||||
// You can't have negative max charges (even zero is a bit goofy but eh)
|
||||
var adjusted = Math.Max(0, value);
|
||||
if (action.Comp.MaxCharges == adjusted)
|
||||
return;
|
||||
|
||||
action.Comp.MaxCharges = adjusted;
|
||||
Dirty(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The next time a charge will be considered to be filled.
|
||||
/// </summary>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -27,7 +28,8 @@ public abstract class SharedChatSystem : EntitySystem
|
||||
public const int VoiceRange = 10; // how far voice goes in world units
|
||||
public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units
|
||||
public const int WhisperMuffledRange = 5; // how far whisper goes at all, in world units
|
||||
public const string DefaultAnnouncementSound = "/Audio/_CP14/Announce/event_boom.ogg"; //CP14 replaced default announce sound
|
||||
public static readonly SoundSpecifier DefaultAnnouncementSound
|
||||
= new SoundPathSpecifier("/Audio/_CP14/Announce/event_boom.ogg"); //CP14 replaced default announce sound
|
||||
|
||||
public static readonly ProtoId<RadioChannelPrototype> CommonChannel = "Common";
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ public sealed class HypospraySystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
_popup.PopupClient(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", target)), target, user);
|
||||
_popup.PopupClient(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", Identity.Entity(target, EntityManager))), target, user);
|
||||
|
||||
if (target != user)
|
||||
{
|
||||
|
||||
@@ -63,6 +63,12 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
[DataField]
|
||||
public bool Recognizable;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this reagent stands out (blood, slime).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Standsout;
|
||||
|
||||
[DataField]
|
||||
public ProtoId<FlavorPrototype>? Flavor;
|
||||
|
||||
|
||||
@@ -15,6 +15,13 @@ public sealed partial class ClothingSpeedModifierComponent : Component
|
||||
|
||||
[DataField]
|
||||
public float SprintModifier = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// An optional required standing state.
|
||||
/// Set to true if you need to be standing, false if you need to not be standing, null if you don't care.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool? Standing;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Inventory;
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -12,10 +13,11 @@ namespace Content.Shared.Clothing;
|
||||
|
||||
public sealed class ClothingSpeedModifierSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examine = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
|
||||
[Dependency] private readonly ItemToggleSystem _toggle = default!;
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly StandingStateSystem _standing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -54,8 +56,13 @@ public sealed class ClothingSpeedModifierSystem : EntitySystem
|
||||
|
||||
private void OnRefreshMoveSpeed(EntityUid uid, ClothingSpeedModifierComponent component, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent> args)
|
||||
{
|
||||
if (_toggle.IsActivated(uid))
|
||||
args.Args.ModifySpeed(component.WalkModifier, component.SprintModifier);
|
||||
if (!_toggle.IsActivated(uid))
|
||||
return;
|
||||
|
||||
if (component.Standing != null && !_standing.IsMatchingState(args.Owner, component.Standing.Value))
|
||||
return;
|
||||
|
||||
args.Args.ModifySpeed(component.WalkModifier, component.SprintModifier);
|
||||
}
|
||||
|
||||
private void OnClothingVerbExamine(EntityUid uid, ClothingSpeedModifierComponent component, GetVerbsEvent<ExamineVerb> args)
|
||||
|
||||
@@ -1,23 +1,64 @@
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Standing;
|
||||
|
||||
namespace Content.Shared.Clothing.EntitySystems;
|
||||
|
||||
/// <remarks>
|
||||
/// We check standing state on all clothing because we don't want you to have anti-gravity unless you're standing.
|
||||
/// This is for balance reasons as it prevents you from wearing anti-grav clothing to cheese being stun cuffed, as
|
||||
/// well as other worse things.
|
||||
/// </remarks>
|
||||
public sealed class AntiGravityClothingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly StandingStateSystem _standing = default!;
|
||||
[Dependency] private readonly SharedGravitySystem _gravity = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<IsWeightlessEvent>>(OnIsWeightless);
|
||||
SubscribeLocalEvent<AntiGravityClothingComponent, ClothingGotEquippedEvent>(OnEquipped);
|
||||
SubscribeLocalEvent<AntiGravityClothingComponent, ClothingGotUnequippedEvent>(OnUnequipped);
|
||||
SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<DownedEvent>>(OnDowned);
|
||||
SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<StoodEvent>>(OnStood);
|
||||
}
|
||||
|
||||
private void OnIsWeightless(Entity<AntiGravityClothingComponent> ent, ref InventoryRelayedEvent<IsWeightlessEvent> args)
|
||||
{
|
||||
if (args.Args.Handled)
|
||||
if (args.Args.Handled || _standing.IsDown(args.Owner))
|
||||
return;
|
||||
|
||||
args.Args.Handled = true;
|
||||
args.Args.IsWeightless = true;
|
||||
}
|
||||
|
||||
private void OnEquipped(Entity<AntiGravityClothingComponent> entity, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
// This clothing item does nothing if we're not standing
|
||||
if (_standing.IsDown(args.Wearer))
|
||||
return;
|
||||
|
||||
_gravity.RefreshWeightless(args.Wearer, true);
|
||||
}
|
||||
|
||||
private void OnUnequipped(Entity<AntiGravityClothingComponent> entity, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
// This clothing item does nothing if we're not standing
|
||||
if (_standing.IsDown(args.Wearer))
|
||||
return;
|
||||
|
||||
_gravity.RefreshWeightless(args.Wearer, false);
|
||||
}
|
||||
|
||||
private void OnDowned(Entity<AntiGravityClothingComponent> entity, ref InventoryRelayedEvent<DownedEvent> args)
|
||||
{
|
||||
_gravity.RefreshWeightless(args.Owner, false);
|
||||
}
|
||||
|
||||
private void OnStood(Entity<AntiGravityClothingComponent> entity, ref InventoryRelayedEvent<StoodEvent> args)
|
||||
{
|
||||
_gravity.RefreshWeightless(args.Owner, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
|
||||
if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp))
|
||||
return;
|
||||
|
||||
if (toggleComp.LifeStage > ComponentLifeStage.Running)
|
||||
if (LifeStage(component.AttachedUid) > EntityLifeStage.MapInitialized)
|
||||
return;
|
||||
|
||||
// As unequipped gets called in the middle of container removal, we cannot call a container-insert without causing issues.
|
||||
|
||||
@@ -150,7 +150,7 @@ public sealed class LoadoutSystem : EntitySystem
|
||||
{
|
||||
// First, randomly pick a startingGear profile from those specified, and equip it.
|
||||
if (startingGear != null && startingGear.Count > 0)
|
||||
_station.EquipStartingGear(uid, _random.Pick(startingGear));
|
||||
_station.EquipStartingGear(uid, _random.Pick(startingGear), false);
|
||||
|
||||
if (loadoutGroups == null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -16,10 +17,4 @@ public sealed partial class MagbootsComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequiresGrid = true;
|
||||
|
||||
/// <summary>
|
||||
/// Slot the clothing has to be worn in to work.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string Slot = "shoes";
|
||||
}
|
||||
|
||||
@@ -32,14 +32,8 @@ public sealed class SharedMagbootsSystem : EntitySystem
|
||||
|
||||
private void OnToggled(Entity<MagbootsComponent> ent, ref ItemToggledEvent args)
|
||||
{
|
||||
var (uid, comp) = ent;
|
||||
// only stick to the floor if being worn in the correct slot
|
||||
if (_container.TryGetContainingContainer((uid, null, null), out var container) &&
|
||||
_inventory.TryGetSlotEntity(container.Owner, comp.Slot, out var worn)
|
||||
&& uid == worn)
|
||||
{
|
||||
if (_container.TryGetContainingContainer((ent.Owner, null, null), out var container))
|
||||
UpdateMagbootEffects(container.Owner, ent, args.Activated);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGotUnequipped(Entity<MagbootsComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||
@@ -58,6 +52,8 @@ public sealed class SharedMagbootsSystem : EntitySystem
|
||||
if (TryComp<MovedByPressureComponent>(user, out var moved))
|
||||
moved.Enabled = !state;
|
||||
|
||||
_gravity.RefreshWeightless(user);
|
||||
|
||||
if (state)
|
||||
_alerts.ShowAlert(user, ent.Comp.MagbootsAlert);
|
||||
else
|
||||
|
||||
@@ -2,7 +2,6 @@ using Content.Shared.Construction.EntitySystems;
|
||||
using Content.Shared.Tools;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Construction.Components
|
||||
{
|
||||
|
||||
@@ -102,6 +102,13 @@ public sealed partial class AnchorableSystem : EntitySystem
|
||||
private void OnAnchoredExamine(EntityUid uid, AnchorableComponent component, ExaminedEvent args)
|
||||
{
|
||||
var isAnchored = Comp<TransformComponent>(uid).Anchored;
|
||||
|
||||
if (isAnchored && (component.Flags & AnchorableFlags.Unanchorable) == 0x0)
|
||||
return;
|
||||
|
||||
if (!isAnchored && (component.Flags & AnchorableFlags.Anchorable) == 0x0)
|
||||
return;
|
||||
|
||||
var messageId = isAnchored ? "examinable-anchored" : "examinable-unanchored";
|
||||
args.PushMarkup(Loc.GetString(messageId, ("target", uid)));
|
||||
}
|
||||
|
||||
@@ -164,12 +164,11 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
|
||||
if (args.Target is { } target && !xformQuery.TryGetComponent(target, out targetXform))
|
||||
return true;
|
||||
|
||||
TransformComponent? usedXform = null;
|
||||
if (args.Used is { } @using && !xformQuery.TryGetComponent(@using, out usedXform))
|
||||
if (args.Used is { } @using && !xformQuery.HasComp(@using))
|
||||
return true;
|
||||
|
||||
// TODO: Re-use existing xform query for these calculations.
|
||||
if (args.BreakOnMove && !(!args.BreakOnWeightlessMove && _gravity.IsWeightless(args.User, xform: userXform)))
|
||||
if (args.BreakOnMove && !(!args.BreakOnWeightlessMove && _gravity.IsWeightless(args.User)))
|
||||
{
|
||||
// Whether the user has moved too much from their original position.
|
||||
if (!_transform.InRange(userXform.Coordinates, doAfter.UserPosition, args.MovementThreshold))
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Drunk;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class DrunkComponent : Component { }
|
||||
11
Content.Shared/Drunk/DrunkStatusEffectComponent.cs
Normal file
11
Content.Shared/Drunk/DrunkStatusEffectComponent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Drunk;
|
||||
|
||||
/// <summary>
|
||||
/// This is used by a status effect entity to apply the <see cref="DrunkComponent"/> to an entity.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class DrunkStatusEffectComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Drunk;
|
||||
|
||||
public abstract class SharedDrunkSystem : EntitySystem
|
||||
{
|
||||
public static readonly ProtoId<StatusEffectPrototype> DrunkKey = "Drunk";
|
||||
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
|
||||
[Dependency] private readonly SharedSlurredSystem _slurredSystem = default!;
|
||||
|
||||
public void TryApplyDrunkenness(EntityUid uid, float boozePower, bool applySlur = true,
|
||||
StatusEffectsComponent? status = null)
|
||||
{
|
||||
if (!Resolve(uid, ref status, false))
|
||||
return;
|
||||
|
||||
if (TryComp<LightweightDrunkComponent>(uid, out var trait))
|
||||
boozePower *= trait.BoozeStrengthMultiplier;
|
||||
|
||||
if (applySlur)
|
||||
{
|
||||
_slurredSystem.DoSlur(uid, TimeSpan.FromSeconds(boozePower), status);
|
||||
}
|
||||
|
||||
if (!_statusEffectsSystem.HasStatusEffect(uid, DrunkKey, status))
|
||||
{
|
||||
_statusEffectsSystem.TryAddStatusEffect<DrunkComponent>(uid, DrunkKey, TimeSpan.FromSeconds(boozePower), true, status);
|
||||
}
|
||||
else
|
||||
{
|
||||
_statusEffectsSystem.TryAddTime(uid, DrunkKey, TimeSpan.FromSeconds(boozePower), status);
|
||||
}
|
||||
}
|
||||
|
||||
public void TryRemoveDrunkenness(EntityUid uid)
|
||||
{
|
||||
_statusEffectsSystem.TryRemoveStatusEffect(uid, DrunkKey);
|
||||
}
|
||||
public void TryRemoveDrunkenessTime(EntityUid uid, double timeRemoved)
|
||||
{
|
||||
_statusEffectsSystem.TryRemoveTime(uid, DrunkKey, TimeSpan.FromSeconds(timeRemoved));
|
||||
}
|
||||
|
||||
}
|
||||
50
Content.Shared/Drunk/SharedDrunkSystem.cs
Normal file
50
Content.Shared/Drunk/SharedDrunkSystem.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Content.Shared.Speech.EntitySystems;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
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!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<LightweightDrunkComponent, DrunkEvent>(OnLightweightDrinking);
|
||||
}
|
||||
|
||||
public void TryApplyDrunkenness(EntityUid uid, TimeSpan boozePower)
|
||||
{
|
||||
var ev = new DrunkEvent(boozePower);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
Status.TryAddStatusEffectDuration(uid, Drunk, ev.Duration);
|
||||
}
|
||||
|
||||
public void TryRemoveDrunkenness(EntityUid uid)
|
||||
{
|
||||
Status.TryRemoveStatusEffect(uid, Drunk);
|
||||
}
|
||||
|
||||
public void TryRemoveDrunkennessTime(EntityUid uid, TimeSpan boozePower)
|
||||
{
|
||||
Status.TryAddTime(uid, Drunk, - boozePower);
|
||||
}
|
||||
|
||||
private void OnLightweightDrinking(Entity<LightweightDrunkComponent> entity, ref DrunkEvent args)
|
||||
{
|
||||
args.Duration *= entity.Comp.BoozeStrengthMultiplier;
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct DrunkEvent(TimeSpan Duration);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.Engineering.Systems;
|
||||
using Content.Shared.Weapons.Melee.Balloon;
|
||||
|
||||
namespace Content.Shared.Engineering.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Implements logic to allow inflatable objects to be safely deflated by <see cref="BalloonPopperComponent"/> items.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The owning entity must have <see cref="DisassembleOnAltVerbComponent"/> to implement the logic.
|
||||
/// </remarks>
|
||||
/// <seealso cref="InflatableSafeDisassemblySystem"/>
|
||||
[RegisterComponent]
|
||||
public sealed partial class InflatableSafeDisassemblyComponent : Component;
|
||||
@@ -19,14 +19,12 @@ public sealed partial class DisassembleOnAltVerbSystem : EntitySystem
|
||||
SubscribeLocalEvent<DisassembleOnAltVerbComponent, GetVerbsEvent<AlternativeVerb>>(AddDisassembleVerb);
|
||||
SubscribeLocalEvent<DisassembleOnAltVerbComponent, DisassembleDoAfterEvent>(OnDisassembleDoAfter);
|
||||
}
|
||||
private void AddDisassembleVerb(Entity<DisassembleOnAltVerbComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess || args.Hands == null)
|
||||
return;
|
||||
|
||||
public void StartDisassembly(Entity<DisassembleOnAltVerbComponent> entity, EntityUid user)
|
||||
{
|
||||
// Doafter setup
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager,
|
||||
args.User,
|
||||
user,
|
||||
entity.Comp.DisassembleTime,
|
||||
new DisassembleDoAfterEvent(),
|
||||
entity,
|
||||
@@ -35,12 +33,22 @@ public sealed partial class DisassembleOnAltVerbSystem : EntitySystem
|
||||
BreakOnMove = true,
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterArgs);
|
||||
}
|
||||
|
||||
private void AddDisassembleVerb(Entity<DisassembleOnAltVerbComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess || args.Hands == null)
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
|
||||
// Actual verb stuff
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
_doAfter.TryStartDoAfter(doAfterArgs);
|
||||
StartDisassembly(entity, user);
|
||||
},
|
||||
Text = Loc.GetString("disassemble-system-verb-disassemble"),
|
||||
Priority = 2
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Shared.Engineering.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Weapons.Melee.Balloon;
|
||||
|
||||
namespace Content.Shared.Engineering.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="InflatableSafeDisassemblyComponent"/>
|
||||
/// </summary>
|
||||
public sealed class InflatableSafeDisassemblySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DisassembleOnAltVerbSystem _disassembleOnAltVerbSystem = null!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = null!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<InflatableSafeDisassemblyComponent, InteractUsingEvent>(InteractHandler);
|
||||
}
|
||||
|
||||
private void InteractHandler(Entity<InflatableSafeDisassemblyComponent> ent, ref InteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!HasComp<BalloonPopperComponent>(args.Used))
|
||||
return;
|
||||
|
||||
_popupSystem.PopupPredicted(
|
||||
Loc.GetString("inflatable-safe-disassembly", ("item", args.Used), ("target", ent.Owner)),
|
||||
ent,
|
||||
args.User);
|
||||
|
||||
_disassembleOnAltVerbSystem.StartDisassembly((ent, Comp<DisassembleOnAltVerbComponent>(ent)), args.User);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,13 @@ public sealed partial class JobCondition : EntityEffectCondition
|
||||
{
|
||||
args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer);
|
||||
|
||||
if ( mindContainer is null
|
||||
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
|
||||
if (mindContainer is null
|
||||
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
|
||||
return false;
|
||||
|
||||
foreach (var roleId in mind.MindRoles)
|
||||
foreach (var roleId in mind.MindRoleContainer.ContainedEntities)
|
||||
{
|
||||
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
|
||||
if (!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
|
||||
continue;
|
||||
|
||||
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
|
||||
|
||||
@@ -9,13 +9,7 @@ public sealed partial class Drunk : EntityEffect
|
||||
/// BoozePower is how long each metabolism cycle will make the drunk effect last for.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BoozePower = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether speech should be slurred.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool SlurSpeech = true;
|
||||
public TimeSpan BoozePower = TimeSpan.FromSeconds(3f);
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
=> Loc.GetString("reagent-effect-guidebook-drunk", ("chance", Probability));
|
||||
@@ -24,11 +18,10 @@ public sealed partial class Drunk : EntityEffect
|
||||
{
|
||||
var boozePower = BoozePower;
|
||||
|
||||
if (args is EntityEffectReagentArgs reagentArgs) {
|
||||
if (args is EntityEffectReagentArgs reagentArgs)
|
||||
boozePower *= reagentArgs.Scale.Float();
|
||||
}
|
||||
|
||||
var drunkSys = args.EntityManager.EntitySysManager.GetEntitySystem<SharedDrunkSystem>();
|
||||
drunkSys.TryApplyDrunkenness(args.TargetEntity, boozePower, SlurSpeech);
|
||||
drunkSys.TryApplyDrunkenness(args.TargetEntity, boozePower);
|
||||
}
|
||||
}
|
||||
|
||||
51
Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs
Normal file
51
Content.Shared/EntityTable/Conditions/HasBudgetCondition.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Content.Shared.EntityTable.EntitySelectors;
|
||||
using Content.Shared.GameTicking.Rules;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityTable.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that only succeeds if a table supplies a sufficient "cost" to a given
|
||||
/// </summary>
|
||||
public sealed partial class HasBudgetCondition : EntityTableCondition
|
||||
{
|
||||
public const string BudgetContextKey = "Budget";
|
||||
|
||||
/// <summary>
|
||||
/// Used for determining the cost for the budget.
|
||||
/// If null, attempts to fetch the cost from the attached selector.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int? CostOverride;
|
||||
|
||||
protected override bool EvaluateImplementation(EntityTableSelector root,
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager proto,
|
||||
EntityTableContext ctx)
|
||||
{
|
||||
if (!ctx.TryGetData<float>(BudgetContextKey, out var budget))
|
||||
return false;
|
||||
|
||||
int cost;
|
||||
if (CostOverride != null)
|
||||
{
|
||||
cost = CostOverride.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (root is not EntSelector entSelector)
|
||||
return false;
|
||||
|
||||
if (!proto.Index(entSelector.Id).TryGetComponent(out DynamicRuleCostComponent? costComponent, entMan.ComponentFactory))
|
||||
{
|
||||
var log = Logger.GetSawmill("HasBudgetCondition");
|
||||
log.Error($"Rule {entSelector.Id} does not have a DynamicRuleCostComponent.");
|
||||
return false;
|
||||
}
|
||||
|
||||
cost = costComponent.Cost;
|
||||
}
|
||||
|
||||
return budget >= cost;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.EntityTable.EntitySelectors;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityTable.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that succeeds only when the specified gamerule has been run under a certain amount of times
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is meant to be attached directly to EntSelector. If it is not, then you'll need to specify what rule
|
||||
/// is being used inside RuleOverride.
|
||||
/// </remarks>
|
||||
public sealed partial class MaxRuleOccurenceCondition : EntityTableCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum amount of times this rule can have already be run.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Max = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The rule that is being checked for occurrences.
|
||||
/// If null, it will use the value on the attached selector.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? RuleOverride;
|
||||
|
||||
protected override bool EvaluateImplementation(EntityTableSelector root,
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager proto,
|
||||
EntityTableContext ctx)
|
||||
{
|
||||
string rule;
|
||||
if (RuleOverride is { } ruleOverride)
|
||||
{
|
||||
rule = ruleOverride;
|
||||
}
|
||||
else
|
||||
{
|
||||
rule = root is EntSelector entSelector
|
||||
? entSelector.Id
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
if (rule == string.Empty)
|
||||
return false;
|
||||
|
||||
var gameTicker = entMan.System<SharedGameTicker>();
|
||||
|
||||
return gameTicker.AllPreviousGameRules.Count(p => p.Item2 == rule) < Max;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Content.Shared.EntityTable.EntitySelectors;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityTable.Conditions;
|
||||
|
||||
public sealed partial class ReoccurrenceDelayCondition : EntityTableCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum amount of times this rule can have already be run.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Delay = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The rule that is being checked for occurrences.
|
||||
/// If null, it will use the value on the attached selector.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? RuleOverride;
|
||||
|
||||
protected override bool EvaluateImplementation(EntityTableSelector root,
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager proto,
|
||||
EntityTableContext ctx)
|
||||
{
|
||||
string rule;
|
||||
if (RuleOverride is { } ruleOverride)
|
||||
{
|
||||
rule = ruleOverride;
|
||||
}
|
||||
else
|
||||
{
|
||||
rule = root is EntSelector entSelector
|
||||
? entSelector.Id
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
if (rule == string.Empty)
|
||||
return false;
|
||||
|
||||
var gameTicker = entMan.System<SharedGameTicker>();
|
||||
|
||||
return gameTicker.AllPreviousGameRules.Any(
|
||||
p => p.Item2 == rule && p.Item1 + Delay <= gameTicker.RoundDuration());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Content.Shared.EntityTable.EntitySelectors;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.EntityTable.Conditions;
|
||||
|
||||
/// <summary>
|
||||
/// Condition that passes only if the current round time falls between the minimum and maximum time values.
|
||||
/// </summary>
|
||||
public sealed partial class RoundDurationCondition : EntityTableCondition
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum time the round must have gone on for this condition to pass.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Min = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount of time the round could go on for this condition to pass.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Max = TimeSpan.MaxValue;
|
||||
|
||||
protected override bool EvaluateImplementation(EntityTableSelector root,
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager proto,
|
||||
EntityTableContext ctx)
|
||||
{
|
||||
var gameTicker = entMan.System<SharedGameTicker>();
|
||||
var duration = gameTicker.RoundDuration();
|
||||
|
||||
return duration >= Min && duration <= Max;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public sealed partial class GroupSelector : EntityTableSelector
|
||||
children.Add(child, child.Weight);
|
||||
}
|
||||
|
||||
if (children.Count == 0)
|
||||
return Array.Empty<EntProtoId>();
|
||||
|
||||
var pick = SharedRandomExtensions.Pick(children, rand);
|
||||
|
||||
return pick.GetSpawns(rand, entMan, proto, ctx);
|
||||
|
||||
@@ -21,6 +21,7 @@ using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Random.Helpers;
|
||||
|
||||
namespace Content.Shared.Flash;
|
||||
|
||||
@@ -204,7 +205,8 @@ public abstract class SharedFlashSystem : EntitySystem
|
||||
foreach (var entity in _entSet)
|
||||
{
|
||||
// TODO: Use RandomPredicted https://github.com/space-wizards/RobustToolbox/pull/5849
|
||||
var rand = new System.Random((int)_timing.CurTick.Value + GetNetEntity(entity).Id);
|
||||
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(entity).Id });
|
||||
var rand = new System.Random(seed);
|
||||
if (!rand.Prob(probability))
|
||||
continue;
|
||||
|
||||
|
||||
@@ -22,11 +22,7 @@ public abstract partial class SharedPuddleSystem : EntitySystem
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
|
||||
private static readonly ProtoId<ReagentPrototype> Blood = "Blood";
|
||||
private static readonly ProtoId<ReagentPrototype> Slime = "Slime";
|
||||
private static readonly ProtoId<ReagentPrototype> CopperBlood = "CopperBlood";
|
||||
|
||||
private static readonly string[] StandoutReagents = [Blood, Slime, CopperBlood];
|
||||
private string[] _standoutReagents = [];
|
||||
|
||||
/// <summary>
|
||||
/// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.
|
||||
@@ -48,9 +44,26 @@ public abstract partial class SharedPuddleSystem : EntitySystem
|
||||
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
|
||||
SubscribeLocalEvent<PuddleComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
|
||||
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
|
||||
CacheStandsout();
|
||||
InitializeSpillable();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
|
||||
{
|
||||
if (ev.WasModified<ReagentPrototype>())
|
||||
CacheStandsout();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to cache standout reagents for future use.
|
||||
/// </summary>
|
||||
private void CacheStandsout()
|
||||
{
|
||||
_standoutReagents = [.. _prototypeManager.EnumeratePrototypes<ReagentPrototype>().Where(x => x.Standsout).Select(x => x.ID)];
|
||||
}
|
||||
|
||||
protected virtual void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
|
||||
{
|
||||
if (args.SolutionId != entity.Comp.SolutionName)
|
||||
@@ -158,10 +171,10 @@ public abstract partial class SharedPuddleSystem : EntitySystem
|
||||
// Kinda EH
|
||||
// Could potentially do alpha per-solution but future problem.
|
||||
|
||||
color = solution.GetColorWithout(_prototypeManager, StandoutReagents);
|
||||
color = solution.GetColorWithout(_prototypeManager, _standoutReagents);
|
||||
color = color.WithAlpha(0.7f);
|
||||
|
||||
foreach (var standout in StandoutReagents)
|
||||
foreach (var standout in _standoutReagents)
|
||||
{
|
||||
var quantity = solution.GetTotalPrototypeQuantity(standout);
|
||||
if (quantity <= FixedPoint2.Zero)
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Content.Shared.Friction
|
||||
|
||||
// If we're not touching the ground, don't use tileFriction.
|
||||
// TODO: Make IsWeightless event-based; we already have grid traversals tracked so just raise events
|
||||
if (body.BodyStatus == BodyStatus.InAir || _gravity.IsWeightless(uid, body, xform) || !xform.Coordinates.IsValid(EntityManager))
|
||||
if (body.BodyStatus == BodyStatus.InAir || _gravity.IsWeightless(uid) || !xform.Coordinates.IsValid(EntityManager))
|
||||
friction = xform.GridUid == null || !_gridQuery.HasComp(xform.GridUid) ? _offGridDamping : _airDamping;
|
||||
else
|
||||
friction = _frictionModifier * GetTileFriction(uid, body, xform);
|
||||
|
||||
71
Content.Shared/GameTicking/Rules/DynamicRuleComponent.cs
Normal file
71
Content.Shared/GameTicking/Rules/DynamicRuleComponent.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Content.Shared.EntityTable.EntitySelectors;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.GameTicking.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Gamerule the spawns multiple antags at intervals based on a budget
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class DynamicRuleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The total budget for antags.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Budget;
|
||||
|
||||
/// <summary>
|
||||
/// The last time budget was updated.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan LastBudgetUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of budget accumulated every second.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BudgetPerSecond = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum or lower bound for budgets to start at.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int StartingBudgetMin = 200;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum or upper bound for budgets to start at.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int StartingBudgetMax = 350;
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the next rule will start
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan NextRuleTime;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum delay between rules
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MinRuleInterval = TimeSpan.FromMinutes(10);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum delay between rules
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MaxRuleInterval = TimeSpan.FromMinutes(30);
|
||||
|
||||
/// <summary>
|
||||
/// A table of rules that are picked from.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityTableSelector Table = new NoneSelector();
|
||||
|
||||
/// <summary>
|
||||
/// The rules that have been spawned
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntityUid> Rules = new();
|
||||
}
|
||||
14
Content.Shared/GameTicking/Rules/DynamicRuleCostComponent.cs
Normal file
14
Content.Shared/GameTicking/Rules/DynamicRuleCostComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Content.Shared.GameTicking.Rules;
|
||||
|
||||
/// <summary>
|
||||
/// Component that tracks how much a rule "costs" for Dynamic
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class DynamicRuleCostComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of budget a rule takes up
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public int Cost;
|
||||
}
|
||||
@@ -15,6 +15,12 @@ namespace Content.Shared.GameTicking
|
||||
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
/// <summary>
|
||||
/// A list storing the start times of all game rules that have been started this round.
|
||||
/// Game rules can be started and stopped at any time, including midround.
|
||||
/// </summary>
|
||||
public abstract IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules { get; }
|
||||
|
||||
// See ideally these would be pulled from the job definition or something.
|
||||
// But this is easier, and at least it isn't hardcoded.
|
||||
//TODO: Move these, they really belong in StationJobsSystem or a cvar.
|
||||
|
||||
@@ -10,20 +10,17 @@ public sealed partial class FloatingVisualsComponent : Component
|
||||
/// <summary>
|
||||
/// How long it takes to go from the bottom of the animation to the top.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public float AnimationTime = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// How far it goes in any direction.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public Vector2 Offset = new(0, 0.2f);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public bool CanFloat = false;
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool CanFloat;
|
||||
|
||||
public readonly string AnimationKey = "gravity";
|
||||
}
|
||||
|
||||
17
Content.Shared/Gravity/GravityAffectedComponent.cs
Normal file
17
Content.Shared/Gravity/GravityAffectedComponent.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Gravity;
|
||||
|
||||
/// <summary>
|
||||
/// This Component allows a target to be considered "weightless" when Weightless is true. Without this component, the
|
||||
/// target will never be weightless.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class GravityAffectedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, this entity will be considered "weightless"
|
||||
/// </summary>
|
||||
[ViewVariables, AutoNetworkedField]
|
||||
public bool Weightless = true;
|
||||
}
|
||||
@@ -5,34 +5,20 @@ using Robust.Shared.Serialization;
|
||||
namespace Content.Shared.Gravity
|
||||
{
|
||||
[RegisterComponent]
|
||||
[AutoGenerateComponentState]
|
||||
[NetworkedComponent]
|
||||
public sealed partial class GravityComponent : Component
|
||||
{
|
||||
[DataField("gravityShakeSound")]
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier GravityShakeSound { get; set; } = new SoundPathSpecifier("/Audio/Effects/alert.ogg");
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool EnabledVV
|
||||
{
|
||||
get => Enabled;
|
||||
set
|
||||
{
|
||||
if (Enabled == value) return;
|
||||
Enabled = value;
|
||||
var ev = new GravityChangedEvent(Owner, value);
|
||||
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner, ref ev);
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("enabled")]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Inherent gravity ensures GravitySystem won't change Enabled according to the gravity generators attached to this entity.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("inherent")]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Inherent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,14 @@ namespace Content.Shared.Gravity;
|
||||
/// </summary>
|
||||
public abstract class SharedFloatingVisualizerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedGravitySystem GravitySystem = default!;
|
||||
[Dependency] private readonly SharedGravitySystem _gravity = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FloatingVisualsComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<GravityChangedEvent>(OnGravityChanged);
|
||||
SubscribeLocalEvent<FloatingVisualsComponent, EntParentChangedMessage>(OnEntParentChanged);
|
||||
SubscribeLocalEvent<FloatingVisualsComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -24,48 +23,28 @@ public abstract class SharedFloatingVisualizerSystem : EntitySystem
|
||||
/// </summary>
|
||||
public virtual void FloatAnimation(EntityUid uid, Vector2 offset, string animationKey, float animationTime, bool stop = false) { }
|
||||
|
||||
protected bool CanFloat(EntityUid uid, FloatingVisualsComponent component, TransformComponent? transform = null)
|
||||
protected bool CanFloat(Entity<FloatingVisualsComponent> entity)
|
||||
{
|
||||
if (!Resolve(uid, ref transform))
|
||||
return false;
|
||||
|
||||
if (transform.MapID == MapId.Nullspace)
|
||||
return false;
|
||||
|
||||
component.CanFloat = GravitySystem.IsWeightless(uid, xform: transform);
|
||||
Dirty(uid, component);
|
||||
return component.CanFloat;
|
||||
entity.Comp.CanFloat = _gravity.IsWeightless(entity.Owner);
|
||||
Dirty(entity);
|
||||
return entity.Comp.CanFloat;
|
||||
}
|
||||
|
||||
private void OnComponentStartup(EntityUid uid, FloatingVisualsComponent component, ComponentStartup args)
|
||||
private void OnComponentStartup(Entity<FloatingVisualsComponent> entity, ref ComponentStartup args)
|
||||
{
|
||||
if (CanFloat(uid, component))
|
||||
FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime);
|
||||
if (CanFloat(entity))
|
||||
FloatAnimation(entity, entity.Comp.Offset, entity.Comp.AnimationKey, entity.Comp.AnimationTime);
|
||||
}
|
||||
|
||||
private void OnGravityChanged(ref GravityChangedEvent args)
|
||||
private void OnWeightlessnessChanged(Entity<FloatingVisualsComponent> entity, ref WeightlessnessChangedEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<FloatingVisualsComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var floating, out var transform))
|
||||
{
|
||||
if (transform.MapID == MapId.Nullspace)
|
||||
continue;
|
||||
if (entity.Comp.CanFloat == args.Weightless)
|
||||
return;
|
||||
|
||||
if (transform.GridUid != args.ChangedGridIndex)
|
||||
continue;
|
||||
entity.Comp.CanFloat = CanFloat(entity);
|
||||
Dirty(entity);
|
||||
|
||||
floating.CanFloat = !args.HasGravity;
|
||||
Dirty(uid, floating);
|
||||
|
||||
if (!args.HasGravity)
|
||||
FloatAnimation(uid, floating.Offset, floating.AnimationKey, floating.AnimationTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEntParentChanged(EntityUid uid, FloatingVisualsComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
var transform = args.Transform;
|
||||
if (CanFloat(uid, component, transform))
|
||||
FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime);
|
||||
if (args.Weightless)
|
||||
FloatAnimation(entity, entity.Comp.Offset, entity.Comp.AnimationKey, entity.Comp.AnimationTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +1,255 @@
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Gravity
|
||||
namespace Content.Shared.Gravity;
|
||||
|
||||
public abstract partial class SharedGravitySystem : EntitySystem
|
||||
{
|
||||
public abstract partial class SharedGravitySystem : EntitySystem
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
|
||||
public static readonly ProtoId<AlertPrototype> WeightlessAlert = "Weightless";
|
||||
|
||||
protected EntityQuery<GravityComponent> GravityQuery;
|
||||
private EntityQuery<GravityAffectedComponent> _weightlessQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
base.Initialize();
|
||||
// Grid Gravity
|
||||
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
|
||||
SubscribeLocalEvent<GravityChangedEvent>(OnGravityChange);
|
||||
|
||||
public static readonly ProtoId<AlertPrototype> WeightlessAlert = "Weightless";
|
||||
// Weightlessness
|
||||
SubscribeLocalEvent<GravityAffectedComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<GravityAffectedComponent, EntParentChangedMessage>(OnEntParentChanged);
|
||||
SubscribeLocalEvent<GravityAffectedComponent, PhysicsBodyTypeChangedEvent>(OnBodyTypeChanged);
|
||||
|
||||
private EntityQuery<GravityComponent> _gravityQuery;
|
||||
// Alerts
|
||||
SubscribeLocalEvent<AlertSyncEvent>(OnAlertsSync);
|
||||
SubscribeLocalEvent<AlertsComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
|
||||
SubscribeLocalEvent<AlertsComponent, EntParentChangedMessage>(OnAlertsParentChange);
|
||||
|
||||
public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, TransformComponent? xform = null)
|
||||
{
|
||||
Resolve(uid, ref body, false);
|
||||
// Impulse
|
||||
SubscribeLocalEvent<GravityAffectedComponent, ShooterImpulseEvent>(OnShooterImpulse);
|
||||
SubscribeLocalEvent<GravityAffectedComponent, ThrowerImpulseEvent>(OnThrowerImpulse);
|
||||
|
||||
if ((body?.BodyType & (BodyType.Static | BodyType.Kinematic)) != 0)
|
||||
return false;
|
||||
GravityQuery = GetEntityQuery<GravityComponent>();
|
||||
_weightlessQuery = GetEntityQuery<GravityAffectedComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
}
|
||||
|
||||
if (TryComp<MovementIgnoreGravityComponent>(uid, out var ignoreGravityComponent))
|
||||
return ignoreGravityComponent.Weightless;
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
UpdateShake();
|
||||
}
|
||||
|
||||
var ev = new IsWeightlessEvent(uid);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
if (ev.Handled)
|
||||
return ev.IsWeightless;
|
||||
public bool IsWeightless(Entity<GravityAffectedComponent?> entity)
|
||||
{
|
||||
// If we can be weightless and are weightless, return true, otherwise return false
|
||||
return _weightlessQuery.Resolve(entity, ref entity.Comp, false) && entity.Comp.Weightless;
|
||||
}
|
||||
|
||||
if (!Resolve(uid, ref xform))
|
||||
return true;
|
||||
private bool GetWeightless(Entity<GravityAffectedComponent, PhysicsComponent?> entity)
|
||||
{
|
||||
if (!_physicsQuery.Resolve(entity, ref entity.Comp2, false))
|
||||
return false;
|
||||
|
||||
// If grid / map has gravity
|
||||
if (EntityGridOrMapHaveGravity((uid, xform)))
|
||||
return false;
|
||||
if (entity.Comp2.BodyType is BodyType.Static or BodyType.Kinematic)
|
||||
return false;
|
||||
|
||||
// Check if something other than the grid or map is overriding our gravity
|
||||
var ev = new IsWeightlessEvent();
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
if (ev.Handled)
|
||||
return ev.IsWeightless;
|
||||
|
||||
return !EntityGridOrMapHaveGravity(entity.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes weightlessness status, needs to be called anytime it would change.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity we are updating the weightless status of</param>
|
||||
public void RefreshWeightless(Entity<GravityAffectedComponent?> entity)
|
||||
{
|
||||
if (!_weightlessQuery.Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
UpdateWeightless(entity!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overload of <see cref="RefreshWeightless(Entity{GravityAffectedComponent?})"/> which also takes a bool for the weightlessness value we want to change to.
|
||||
/// This method should only be called if there is no chance something can override the weightless value you're trying to change to.
|
||||
/// This is really only the case if you're applying a weightless value that overrides non-conditionally from events or are a grid with the gravity component.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity we are updating the weightless status of</param>
|
||||
/// <param name="weightless">The weightless value we are trying to change to, helps avoid needless networking</param>
|
||||
public void RefreshWeightless(Entity<GravityAffectedComponent?> entity, bool weightless)
|
||||
{
|
||||
if (!_weightlessQuery.Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
// Only update if we're changing our weightless status
|
||||
if (entity.Comp.Weightless == weightless)
|
||||
return;
|
||||
|
||||
UpdateWeightless(entity!);
|
||||
}
|
||||
|
||||
private void UpdateWeightless(Entity<GravityAffectedComponent> entity)
|
||||
{
|
||||
var newWeightless = GetWeightless(entity);
|
||||
|
||||
// Don't network or raise events if it's not changing
|
||||
if (newWeightless == entity.Comp.Weightless)
|
||||
return;
|
||||
|
||||
entity.Comp.Weightless = newWeightless;
|
||||
Dirty(entity);
|
||||
|
||||
var ev = new WeightlessnessChangedEvent(entity.Comp.Weightless);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<GravityAffectedComponent> entity, ref MapInitEvent args)
|
||||
{
|
||||
RefreshWeightless((entity.Owner, entity.Comp));
|
||||
}
|
||||
|
||||
private void OnWeightlessnessChanged(Entity<AlertsComponent> entity, ref WeightlessnessChangedEvent args)
|
||||
{
|
||||
if (args.Weightless)
|
||||
_alerts.ShowAlert(entity, WeightlessAlert);
|
||||
else
|
||||
_alerts.ClearAlert(entity, WeightlessAlert);
|
||||
}
|
||||
|
||||
private void OnEntParentChanged(Entity<GravityAffectedComponent> entity, ref EntParentChangedMessage args)
|
||||
{
|
||||
// If we've moved but are still on the same grid, then don't do anything.
|
||||
if (args.OldParent == args.Transform.GridUid)
|
||||
return;
|
||||
|
||||
RefreshWeightless((entity.Owner, entity.Comp));
|
||||
}
|
||||
|
||||
private void OnBodyTypeChanged(Entity<GravityAffectedComponent> entity, ref PhysicsBodyTypeChangedEvent args)
|
||||
{
|
||||
// No need to update weightlessness if we're not weightless and we're a body type that can't be weightless
|
||||
if (args.New is BodyType.Static or BodyType.Kinematic && entity.Comp.Weightless == false)
|
||||
return;
|
||||
|
||||
RefreshWeightless((entity.Owner, entity.Comp));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given entity is currently standing on a grid or map that supports having gravity at all.
|
||||
/// </summary>
|
||||
public bool EntityOnGravitySupportingGridOrMap(Entity<TransformComponent?> entity)
|
||||
{
|
||||
entity.Comp ??= Transform(entity);
|
||||
|
||||
return GravityQuery.HasComp(entity.Comp.GridUid) ||
|
||||
GravityQuery.HasComp(entity.Comp.MapUid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given entity is currently standing on a grid or map that has gravity of some kind.
|
||||
/// </summary>
|
||||
public bool EntityGridOrMapHaveGravity(Entity<TransformComponent?> entity)
|
||||
{
|
||||
entity.Comp ??= Transform(entity);
|
||||
|
||||
// DO NOT SET TO WEIGHTLESS IF THEY'RE IN NULL-SPACE
|
||||
// TODO: If entities actually properly pause when leaving PVS rather than entering null-space this can probably go.
|
||||
if (entity.Comp.MapID == MapId.Nullspace)
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given entity is currently standing on a grid or map that supports having gravity at all.
|
||||
/// </summary>
|
||||
public bool EntityOnGravitySupportingGridOrMap(Entity<TransformComponent?> entity)
|
||||
return GravityQuery.TryComp(entity.Comp.GridUid, out var gravity) && gravity.Enabled ||
|
||||
GravityQuery.TryComp(entity.Comp.MapUid, out var mapGravity) && mapGravity.Enabled;
|
||||
}
|
||||
|
||||
private void OnGravityChange(ref GravityChangedEvent args)
|
||||
{
|
||||
var gravity = AllEntityQuery<GravityAffectedComponent, TransformComponent>();
|
||||
while(gravity.MoveNext(out var uid, out var weightless, out var xform))
|
||||
{
|
||||
entity.Comp ??= Transform(entity);
|
||||
if (xform.GridUid != args.ChangedGridIndex)
|
||||
continue;
|
||||
|
||||
return _gravityQuery.HasComp(entity.Comp.GridUid) ||
|
||||
_gravityQuery.HasComp(entity.Comp.MapUid);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given entity is currently standing on a grid or map that has gravity of some kind.
|
||||
/// </summary>
|
||||
public bool EntityGridOrMapHaveGravity(Entity<TransformComponent?> entity)
|
||||
{
|
||||
entity.Comp ??= Transform(entity);
|
||||
|
||||
return _gravityQuery.TryComp(entity.Comp.GridUid, out var gravity) && gravity.Enabled ||
|
||||
_gravityQuery.TryComp(entity.Comp.MapUid, out var mapGravity) && mapGravity.Enabled;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
|
||||
SubscribeLocalEvent<AlertSyncEvent>(OnAlertsSync);
|
||||
SubscribeLocalEvent<AlertsComponent, EntParentChangedMessage>(OnAlertsParentChange);
|
||||
SubscribeLocalEvent<GravityChangedEvent>(OnGravityChange);
|
||||
SubscribeLocalEvent<GravityComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<GravityComponent, ComponentHandleState>(OnHandleState);
|
||||
|
||||
_gravityQuery = GetEntityQuery<GravityComponent>();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
UpdateShake();
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, GravityComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not GravityComponentState state)
|
||||
return;
|
||||
|
||||
if (component.EnabledVV == state.Enabled)
|
||||
return;
|
||||
component.EnabledVV = state.Enabled;
|
||||
var ev = new GravityChangedEvent(uid, component.EnabledVV);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, GravityComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new GravityComponentState(component.EnabledVV);
|
||||
}
|
||||
|
||||
private void OnGravityChange(ref GravityChangedEvent ev)
|
||||
{
|
||||
var alerts = AllEntityQuery<AlertsComponent, TransformComponent>();
|
||||
while(alerts.MoveNext(out var uid, out _, out var xform))
|
||||
{
|
||||
if (xform.GridUid != ev.ChangedGridIndex)
|
||||
continue;
|
||||
|
||||
if (!ev.HasGravity)
|
||||
{
|
||||
_alerts.ShowAlert(uid, WeightlessAlert);
|
||||
}
|
||||
else
|
||||
{
|
||||
_alerts.ClearAlert(uid, WeightlessAlert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAlertsSync(AlertSyncEvent ev)
|
||||
{
|
||||
if (IsWeightless(ev.Euid))
|
||||
{
|
||||
_alerts.ShowAlert(ev.Euid, WeightlessAlert);
|
||||
}
|
||||
else
|
||||
{
|
||||
_alerts.ClearAlert(ev.Euid, WeightlessAlert);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAlertsParentChange(EntityUid uid, AlertsComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
if (IsWeightless(uid))
|
||||
{
|
||||
_alerts.ShowAlert(uid, WeightlessAlert);
|
||||
}
|
||||
else
|
||||
{
|
||||
_alerts.ClearAlert(uid, WeightlessAlert);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGridInit(GridInitializeEvent ev)
|
||||
{
|
||||
EnsureComp<GravityComponent>(ev.EntityUid);
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
private sealed class GravityComponentState : ComponentState
|
||||
{
|
||||
public bool Enabled { get; }
|
||||
|
||||
public GravityComponentState(bool enabled)
|
||||
{
|
||||
Enabled = enabled;
|
||||
}
|
||||
RefreshWeightless((uid, weightless), !args.HasGravity);
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct IsWeightlessEvent(EntityUid Entity, bool IsWeightless = false, bool Handled = false) : IInventoryRelayEvent
|
||||
private void OnAlertsSync(AlertSyncEvent ev)
|
||||
{
|
||||
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
|
||||
if (IsWeightless(ev.Euid))
|
||||
_alerts.ShowAlert(ev.Euid, WeightlessAlert);
|
||||
else
|
||||
_alerts.ClearAlert(ev.Euid, WeightlessAlert);
|
||||
}
|
||||
|
||||
private void OnAlertsParentChange(EntityUid uid, AlertsComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
if (IsWeightless(uid))
|
||||
_alerts.ShowAlert(uid, WeightlessAlert);
|
||||
else
|
||||
_alerts.ClearAlert(uid, WeightlessAlert);
|
||||
}
|
||||
|
||||
private void OnGridInit(GridInitializeEvent ev)
|
||||
{
|
||||
EnsureComp<GravityComponent>(ev.EntityUid);
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
private sealed class GravityComponentState : ComponentState
|
||||
{
|
||||
public bool Enabled { get; }
|
||||
|
||||
public GravityComponentState(bool enabled)
|
||||
{
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThrowerImpulse(Entity<GravityAffectedComponent> entity, ref ThrowerImpulseEvent args)
|
||||
{
|
||||
args.Push = true;
|
||||
}
|
||||
|
||||
private void OnShooterImpulse(Entity<GravityAffectedComponent> entity, ref ShooterImpulseEvent args)
|
||||
{
|
||||
args.Push = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised to determine if an entity's weightlessness is being overwritten by a component or item with a component.
|
||||
/// </summary>
|
||||
/// <param name="IsWeightless">Whether we should be weightless</param>
|
||||
/// <param name="Handled">Whether something is trying to override our weightlessness</param>
|
||||
[ByRefEvent]
|
||||
public record struct IsWeightlessEvent(bool IsWeightless = false, bool Handled = false) : IInventoryRelayEvent
|
||||
{
|
||||
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on an entity when their weightless status changes.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct WeightlessnessChangedEvent(bool Weightless);
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Database;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory.VirtualItem;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
@@ -20,6 +21,7 @@ public abstract partial class SharedHandsSystem
|
||||
private void InitializeDrop()
|
||||
{
|
||||
SubscribeLocalEvent<HandsComponent, EntRemovedFromContainerMessage>(HandleEntityRemoved);
|
||||
SubscribeLocalEvent<HandsComponent, EntityStorageIntoContainerAttemptEvent>(OnEntityStorageDump);
|
||||
}
|
||||
|
||||
protected virtual void HandleEntityRemoved(EntityUid uid, HandsComponent hands, EntRemovedFromContainerMessage args)
|
||||
@@ -39,6 +41,14 @@ public abstract partial class SharedHandsSystem
|
||||
_virtualSystem.DeleteVirtualItem((args.Entity, @virtual), uid);
|
||||
}
|
||||
|
||||
|
||||
private void OnEntityStorageDump(Entity<HandsComponent> ent, ref EntityStorageIntoContainerAttemptEvent args)
|
||||
{
|
||||
// If you're physically carrying an EntityStroage which tries to dump its contents out,
|
||||
// we want those contents to fall to the floor.
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private bool ShouldIgnoreRestrictions(EntityUid user)
|
||||
{
|
||||
//Checks if the Entity is something that shouldn't care about drop distance or walls ie Aghost
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Implants.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to implants with the see <see cref="SubdermalImplantComponent"/>.
|
||||
/// When implanted it will cause other implants in the whitelist to be deleted and thus replaced.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ReplacementImplantComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whitelist for which implants to delete.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public EntityWhitelist Whitelist = new();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Implants.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Handles emptying the implant's <see cref="StorageComponent"/> when the implant is removed.
|
||||
/// Without this the contents would be deleted.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class StorageImplantComponent : Component;
|
||||
@@ -16,13 +16,21 @@ public sealed partial class SubdermalImplantComponent : Component
|
||||
/// <summary>
|
||||
/// Used where you want the implant to grant the owner an instant action.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("implantAction")]
|
||||
[DataField]
|
||||
public EntProtoId? ImplantAction;
|
||||
|
||||
/// <summary>
|
||||
/// The provided action entity.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? Action;
|
||||
|
||||
/// <summary>
|
||||
/// Components to add/remove to the implantee when the implant is injected/extracted.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry ImplantComponents = new();
|
||||
|
||||
/// <summary>
|
||||
/// The entity this implant is inside
|
||||
/// </summary>
|
||||
@@ -32,8 +40,7 @@ public sealed partial class SubdermalImplantComponent : Component
|
||||
/// <summary>
|
||||
/// Should this implant be removeable?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("permanent"), AutoNetworkedField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Permanent = false;
|
||||
|
||||
/// <summary>
|
||||
@@ -61,23 +68,20 @@ public sealed partial class SubdermalImplantComponent : Component
|
||||
/// <summary>
|
||||
/// Used for opening the storage implant via action.
|
||||
/// </summary>
|
||||
public sealed partial class OpenStorageImplantEvent : InstantActionEvent
|
||||
{
|
||||
|
||||
}
|
||||
/// <remarks>
|
||||
/// TODO: Delete this and just add a ToggleUIOnTriggerComponent
|
||||
/// </remarks>
|
||||
public sealed partial class OpenStorageImplantEvent : InstantActionEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Used for triggering trigger events on the implant via action
|
||||
/// </summary>
|
||||
public sealed partial class ActivateImplantEvent : InstantActionEvent
|
||||
{
|
||||
|
||||
}
|
||||
public sealed partial class ActivateImplantEvent : InstantActionEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Used for opening the uplink implant via action.
|
||||
/// </summary>
|
||||
public sealed partial class OpenUplinkImplantEvent : InstantActionEvent
|
||||
{
|
||||
|
||||
}
|
||||
/// <remarks>
|
||||
/// TODO: Delete this and just add a ToggleUIOnTriggerComponent
|
||||
/// </remarks>
|
||||
public sealed partial class OpenUplinkImplantEvent : InstantActionEvent;
|
||||
|
||||
34
Content.Shared/Implants/ReplacementImplantSystem.cs
Normal file
34
Content.Shared/Implants/ReplacementImplantSystem.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Shared.Implants;
|
||||
|
||||
public sealed class ReplacementImplantSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ReplacementImplantComponent, ImplantImplantedEvent>(OnImplantImplanted);
|
||||
}
|
||||
|
||||
private void OnImplantImplanted(Entity<ReplacementImplantComponent> ent, ref ImplantImplantedEvent args)
|
||||
{
|
||||
if (!_container.TryGetContainer(args.Implanted, ImplanterComponent.ImplantSlotId, out var implantContainer))
|
||||
return;
|
||||
|
||||
foreach (var implant in implantContainer.ContainedEntities)
|
||||
{
|
||||
if (implant == ent.Owner)
|
||||
continue; // don't delete the replacement
|
||||
|
||||
if (_whitelist.IsWhitelistPass(ent.Comp.Whitelist, implant))
|
||||
PredictedQueueDel(implant);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ public abstract class SharedImplanterSystem : EntitySystem
|
||||
//Set to draw mode if not implant only
|
||||
public void Implant(EntityUid user, EntityUid target, EntityUid implanter, ImplanterComponent component)
|
||||
{
|
||||
if (!CanImplant(user, target, implanter, component, out var implant, out var implantComp))
|
||||
if (!CanImplant(user, target, implanter, component, out var implant, out _))
|
||||
return;
|
||||
|
||||
// Check if we are trying to implant a implant which is already implanted
|
||||
@@ -137,7 +137,6 @@ public abstract class SharedImplanterSystem : EntitySystem
|
||||
|
||||
if (component.ImplanterSlot.ContainerSlot != null)
|
||||
_container.Remove(implant.Value, component.ImplanterSlot.ContainerSlot);
|
||||
implantComp.ImplantedEntity = target;
|
||||
implantContainer.OccludesLight = false;
|
||||
_container.Insert(implant.Value, implantContainer);
|
||||
|
||||
@@ -280,7 +279,6 @@ public abstract class SharedImplanterSystem : EntitySystem
|
||||
private void DrawImplantIntoImplanter(EntityUid implanter, EntityUid target, EntityUid implant, BaseContainer implantContainer, ContainerSlot implanterContainer, SubdermalImplantComponent implantComp)
|
||||
{
|
||||
_container.Remove(implant, implantContainer);
|
||||
implantComp.ImplantedEntity = null;
|
||||
_container.Insert(implant, implanterContainer);
|
||||
|
||||
var ev = new TransferDnaEvent { Donor = target, Recipient = implanter };
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mobs;
|
||||
|
||||
namespace Content.Shared.Implants;
|
||||
|
||||
public abstract partial class SharedSubdermalImplantSystem
|
||||
{
|
||||
public void InitializeRelay()
|
||||
{
|
||||
SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
|
||||
SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent);
|
||||
SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Relays events from the implanted to the implant.
|
||||
/// </summary>
|
||||
private void RelayToImplantEvent<T>(EntityUid uid, ImplantedComponent component, T args) where T : notnull
|
||||
{
|
||||
if (!_container.TryGetContainer(uid, ImplanterComponent.ImplantSlotId, out var implantContainer))
|
||||
return;
|
||||
|
||||
var relayEv = new ImplantRelayEvent<T>(args, uid);
|
||||
foreach (var implant in implantContainer.ContainedEntities)
|
||||
{
|
||||
if (args is HandledEntityEventArgs { Handled: true })
|
||||
return;
|
||||
|
||||
RaiseLocalEvent(implant, relayEv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for relaying events from an implanted entity to their implants.
|
||||
/// </summary>
|
||||
public sealed class ImplantRelayEvent<T> where T : notnull
|
||||
{
|
||||
public readonly T Event;
|
||||
|
||||
public readonly EntityUid ImplantedEntity;
|
||||
|
||||
public ImplantRelayEvent(T ev, EntityUid implantedEntity)
|
||||
{
|
||||
Event = ev;
|
||||
ImplantedEntity = implantedEntity;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,76 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Tag;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Implants;
|
||||
|
||||
public abstract class SharedSubdermalImplantSystem : EntitySystem
|
||||
public abstract partial class SharedSubdermalImplantSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
|
||||
public const string BaseStorageId = "storagebase";
|
||||
|
||||
private static readonly ProtoId<TagPrototype> MicroBombTag = "MicroBomb";
|
||||
private static readonly ProtoId<TagPrototype> MacroBombTag = "MacroBomb";
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
InitializeRelay();
|
||||
|
||||
SubscribeLocalEvent<SubdermalImplantComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
|
||||
SubscribeLocalEvent<SubdermalImplantComponent, ContainerGettingRemovedAttemptEvent>(OnRemoveAttempt);
|
||||
SubscribeLocalEvent<SubdermalImplantComponent, EntGotRemovedFromContainerMessage>(OnRemove);
|
||||
|
||||
SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
|
||||
SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent);
|
||||
SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent);
|
||||
}
|
||||
|
||||
private void OnInsert(EntityUid uid, SubdermalImplantComponent component, EntGotInsertedIntoContainerMessage args)
|
||||
private void OnInsert(Entity<SubdermalImplantComponent> ent, ref EntGotInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (component.ImplantedEntity == null)
|
||||
// The results of the container change are already networked on their own
|
||||
if (_timing.ApplyingState)
|
||||
return;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(component.ImplantAction))
|
||||
{
|
||||
_actionsSystem.AddAction(component.ImplantedEntity.Value, ref component.Action, component.ImplantAction, uid);
|
||||
}
|
||||
if (args.Container.ID != ImplanterComponent.ImplantSlotId)
|
||||
return;
|
||||
|
||||
// replace micro bomb with macro bomb
|
||||
// TODO: this shouldn't be hardcoded here
|
||||
if (_container.TryGetContainer(component.ImplantedEntity.Value, ImplanterComponent.ImplantSlotId, out var implantContainer) && _tag.HasTag(uid, MacroBombTag))
|
||||
{
|
||||
foreach (var implant in implantContainer.ContainedEntities)
|
||||
{
|
||||
if (_tag.HasTag(implant, MicroBombTag))
|
||||
{
|
||||
_container.Remove(implant, implantContainer);
|
||||
PredictedQueueDel(implant);
|
||||
}
|
||||
}
|
||||
}
|
||||
ent.Comp.ImplantedEntity = args.Container.Owner;
|
||||
Dirty(ent);
|
||||
|
||||
var ev = new ImplantImplantedEvent(uid, component.ImplantedEntity.Value);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
EntityManager.AddComponents(ent.Comp.ImplantedEntity.Value, ent.Comp.ImplantComponents);
|
||||
if (ent.Comp.ImplantAction != null)
|
||||
_actions.AddAction(ent.Comp.ImplantedEntity.Value, ref ent.Comp.Action, ent.Comp.ImplantAction, ent.Owner);
|
||||
|
||||
var ev = new ImplantImplantedEvent(ent.Owner, ent.Comp.ImplantedEntity.Value);
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
}
|
||||
|
||||
private void OnRemoveAttempt(EntityUid uid, SubdermalImplantComponent component, ContainerGettingRemovedAttemptEvent args)
|
||||
private void OnRemoveAttempt(Entity<SubdermalImplantComponent> ent, ref ContainerGettingRemovedAttemptEvent args)
|
||||
{
|
||||
if (component.Permanent && component.ImplantedEntity != null)
|
||||
if (ent.Comp.Permanent && ent.Comp.ImplantedEntity != null)
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnRemove(EntityUid uid, SubdermalImplantComponent component, EntGotRemovedFromContainerMessage args)
|
||||
private void OnRemove(Entity<SubdermalImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
|
||||
{
|
||||
if (component.ImplantedEntity == null || Terminating(component.ImplantedEntity.Value))
|
||||
// The results of the container change are already networked on their own
|
||||
if (_timing.ApplyingState)
|
||||
return;
|
||||
|
||||
if (component.ImplantAction != null)
|
||||
_actionsSystem.RemoveProvidedActions(component.ImplantedEntity.Value, uid);
|
||||
|
||||
if (!_container.TryGetContainer(uid, BaseStorageId, out var storageImplant))
|
||||
if (args.Container.ID != ImplanterComponent.ImplantSlotId)
|
||||
return;
|
||||
|
||||
var containedEntites = storageImplant.ContainedEntities.ToArray();
|
||||
if (ent.Comp.ImplantedEntity == null || Terminating(ent.Comp.ImplantedEntity.Value))
|
||||
return;
|
||||
|
||||
foreach (var entity in containedEntites)
|
||||
{
|
||||
_transformSystem.DropNextTo(entity, uid);
|
||||
}
|
||||
EntityManager.RemoveComponents(ent.Comp.ImplantedEntity.Value, ent.Comp.ImplantComponents);
|
||||
_actions.RemoveAction(ent.Comp.ImplantedEntity.Value, ent.Comp.Action);
|
||||
ent.Comp.Action = null;
|
||||
|
||||
var ev = new ImplantRemovedEvent(ent.Owner, ent.Comp.ImplantedEntity.Value);
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
|
||||
ent.Comp.ImplantedEntity = null;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -106,23 +92,26 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
|
||||
/// <returns>
|
||||
/// The implant, if it was successfully created. Otherwise, null.
|
||||
/// </returns>>
|
||||
public EntityUid? AddImplant(EntityUid uid, String implantId)
|
||||
public EntityUid? AddImplant(EntityUid target, EntProtoId implantId)
|
||||
{
|
||||
var coords = Transform(uid).Coordinates;
|
||||
var ent = Spawn(implantId, coords);
|
||||
if (_net.IsClient)
|
||||
return null; // can't interact with predicted spawns yet
|
||||
|
||||
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
|
||||
var coords = Transform(target).Coordinates;
|
||||
var implant = Spawn(implantId, coords);
|
||||
|
||||
if (TryComp<SubdermalImplantComponent>(implant, out var implantComp))
|
||||
{
|
||||
ForceImplant(uid, ent, implant);
|
||||
ForceImplant(target, (implant, implantComp));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}");
|
||||
Del(ent);
|
||||
Log.Warning($"Tried to inject implant '{implantId}' without SubdermalImplantComponent into {ToPrettyString(target):implanted}");
|
||||
Del(implant);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ent;
|
||||
return implant;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -131,15 +120,16 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <param name="target">The entity to be implanted</param>
|
||||
/// <param name="implant"> The implant</param>
|
||||
/// <param name="component">The implant component</param>
|
||||
public void ForceImplant(EntityUid target, EntityUid implant, SubdermalImplantComponent component)
|
||||
public void ForceImplant(EntityUid target, Entity<SubdermalImplantComponent?> implant)
|
||||
{
|
||||
if (!Resolve(implant, ref implant.Comp))
|
||||
return;
|
||||
|
||||
//If the target doesn't have the implanted component, add it.
|
||||
var implantedComp = EnsureComp<ImplantedComponent>(target);
|
||||
var implantContainer = implantedComp.ImplantContainer;
|
||||
|
||||
component.ImplantedEntity = target;
|
||||
_container.Insert(implant, implantContainer);
|
||||
implant.Comp.ImplantedEntity = target;
|
||||
_container.Insert(implant.Owner, implantedComp.ImplantContainer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -147,60 +137,25 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <param name="target">the implanted entity</param>
|
||||
/// <param name="implant">the implant</param>
|
||||
[PublicAPI]
|
||||
public void ForceRemove(EntityUid target, EntityUid implant)
|
||||
public void ForceRemove(Entity<ImplantedComponent?> target, EntityUid implant)
|
||||
{
|
||||
if (!TryComp<ImplantedComponent>(target, out var implanted))
|
||||
if (!Resolve(target, ref target.Comp))
|
||||
return;
|
||||
|
||||
var implantContainer = implanted.ImplantContainer;
|
||||
|
||||
_container.Remove(implant, implantContainer);
|
||||
QueueDel(implant);
|
||||
_container.Remove(implant, target.Comp.ImplantContainer);
|
||||
PredictedQueueDel(implant);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and deletes implants by force
|
||||
/// </summary>
|
||||
/// <param name="target">The entity to have implants removed</param>
|
||||
[PublicAPI]
|
||||
public void WipeImplants(EntityUid target)
|
||||
public void WipeImplants(Entity<ImplantedComponent?> target)
|
||||
{
|
||||
if (!TryComp<ImplantedComponent>(target, out var implanted))
|
||||
if (!Resolve(target, ref target.Comp, false))
|
||||
return;
|
||||
|
||||
var implantContainer = implanted.ImplantContainer;
|
||||
|
||||
_container.CleanContainer(implantContainer);
|
||||
}
|
||||
|
||||
//Relays from the implanted to the implant
|
||||
private void RelayToImplantEvent<T>(EntityUid uid, ImplantedComponent component, T args) where T : notnull
|
||||
{
|
||||
if (!_container.TryGetContainer(uid, ImplanterComponent.ImplantSlotId, out var implantContainer))
|
||||
return;
|
||||
|
||||
var relayEv = new ImplantRelayEvent<T>(args, uid);
|
||||
foreach (var implant in implantContainer.ContainedEntities)
|
||||
{
|
||||
if (args is HandledEntityEventArgs { Handled : true })
|
||||
return;
|
||||
|
||||
RaiseLocalEvent(implant, relayEv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ImplantRelayEvent<T> where T : notnull
|
||||
{
|
||||
public readonly T Event;
|
||||
|
||||
public readonly EntityUid ImplantedEntity;
|
||||
|
||||
public ImplantRelayEvent(T ev, EntityUid implantedEntity)
|
||||
{
|
||||
Event = ev;
|
||||
ImplantedEntity = implantedEntity;
|
||||
_container.CleanContainer(target.Comp.ImplantContainer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,12 +167,30 @@ public sealed class ImplantRelayEvent<T> where T : notnull
|
||||
/// implant implant implant implant
|
||||
/// </remarks>
|
||||
[ByRefEvent]
|
||||
public readonly struct ImplantImplantedEvent
|
||||
public readonly record struct ImplantImplantedEvent
|
||||
{
|
||||
public readonly EntityUid Implant;
|
||||
public readonly EntityUid? Implanted;
|
||||
public readonly EntityUid Implanted;
|
||||
|
||||
public ImplantImplantedEvent(EntityUid implant, EntityUid? implanted)
|
||||
public ImplantImplantedEvent(EntityUid implant, EntityUid implanted)
|
||||
{
|
||||
Implant = implant;
|
||||
Implanted = implanted;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that is raised whenever an implant is removed from someone.
|
||||
/// Raised on the the implant entity.
|
||||
/// </summary>
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly record struct ImplantRemovedEvent
|
||||
{
|
||||
public readonly EntityUid Implant;
|
||||
public readonly EntityUid Implanted;
|
||||
|
||||
public ImplantRemovedEvent(EntityUid implant, EntityUid implanted)
|
||||
{
|
||||
Implant = implant;
|
||||
Implanted = implanted;
|
||||
|
||||
36
Content.Shared/Implants/StorageImplantSystem.cs
Normal file
36
Content.Shared/Implants/StorageImplantSystem.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Shared.Implants;
|
||||
|
||||
public sealed class StorageImplantSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StorageImplantComponent, ImplantRemovedEvent>(OnImplantRemoved);
|
||||
}
|
||||
|
||||
private void OnImplantRemoved(Entity<StorageImplantComponent> ent, ref ImplantRemovedEvent args)
|
||||
{
|
||||
if (_net.IsClient)
|
||||
return; // TODO: RandomPredicted and DropNextToPredicted
|
||||
|
||||
if (!_container.TryGetContainer(ent.Owner, StorageComponent.ContainerId, out var storageImplant))
|
||||
return;
|
||||
|
||||
var contained = storageImplant.ContainedEntities.ToArray();
|
||||
foreach (var entity in contained)
|
||||
{
|
||||
_transform.DropNextTo(entity, ent.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ public sealed class SmartEquipSystem : EntitySystem
|
||||
}
|
||||
|
||||
_hands.TryDrop((uid, hands), hands.ActiveHandId!);
|
||||
_storage.Insert(slotItem, handItem.Value, out var stacked, out _);
|
||||
_storage.Insert(slotItem, handItem.Value, out var stacked, out _, user: uid);
|
||||
|
||||
// if the hand item stacked with the things in inventory, but there's no more space left for the rest
|
||||
// of the stack, place the stack back in hand rather than dropping it on the floor
|
||||
|
||||
@@ -27,6 +27,7 @@ using Content.Shared.Overlays;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Strip.Components;
|
||||
using Content.Shared.Temperature;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -66,6 +67,8 @@ public partial class InventorySystem
|
||||
SubscribeLocalEvent<InventoryComponent, IsUnequippingTargetAttemptEvent>(RelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, ChameleonControllerOutfitSelectedEvent>(RelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, StoodEvent>(RelayInventoryEvent);
|
||||
SubscribeLocalEvent<InventoryComponent, DownedEvent>(RelayInventoryEvent);
|
||||
|
||||
// by-ref events
|
||||
SubscribeLocalEvent<InventoryComponent, RefreshFrictionModifiersEvent>(RefRelayInventoryEvent);
|
||||
@@ -123,7 +126,7 @@ public partial class InventorySystem
|
||||
return;
|
||||
|
||||
// this copies the by-ref event if it is a struct
|
||||
var ev = new InventoryRelayedEvent<T>(args);
|
||||
var ev = new InventoryRelayedEvent<T>(args, inventory.Owner);
|
||||
var enumerator = new InventorySlotEnumerator(inventory, args.TargetSlots);
|
||||
while (enumerator.NextItem(out var item))
|
||||
{
|
||||
@@ -139,7 +142,7 @@ public partial class InventorySystem
|
||||
if (args.TargetSlots == SlotFlags.NONE)
|
||||
return;
|
||||
|
||||
var ev = new InventoryRelayedEvent<T>(args);
|
||||
var ev = new InventoryRelayedEvent<T>(args, inventory.Owner);
|
||||
var enumerator = new InventorySlotEnumerator(inventory, args.TargetSlots);
|
||||
while (enumerator.NextItem(out var item))
|
||||
{
|
||||
@@ -150,7 +153,7 @@ public partial class InventorySystem
|
||||
private void OnGetEquipmentVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<EquipmentVerb> args)
|
||||
{
|
||||
// Automatically relay stripping related verbs to all equipped clothing.
|
||||
var ev = new InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>>(args);
|
||||
var ev = new InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>>(args, uid);
|
||||
var enumerator = new InventorySlotEnumerator(component);
|
||||
while (enumerator.NextItem(out var item, out var slotDef))
|
||||
{
|
||||
@@ -162,7 +165,7 @@ public partial class InventorySystem
|
||||
private void OnGetInnateVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<InnateVerb> args)
|
||||
{
|
||||
// Automatically relay stripping related verbs to all equipped clothing.
|
||||
var ev = new InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>(args);
|
||||
var ev = new InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>(args, uid);
|
||||
var enumerator = new InventorySlotEnumerator(component, SlotFlags.WITHOUT_POCKET);
|
||||
while (enumerator.NextItem(out var item))
|
||||
{
|
||||
@@ -185,9 +188,12 @@ public sealed class InventoryRelayedEvent<TEvent> : EntityEventArgs
|
||||
{
|
||||
public TEvent Args;
|
||||
|
||||
public InventoryRelayedEvent(TEvent args)
|
||||
public EntityUid Owner;
|
||||
|
||||
public InventoryRelayedEvent(TEvent args, EntityUid owner)
|
||||
{
|
||||
Args = args;
|
||||
Owner = owner;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,143 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Kitchen.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used to mark entity that should act as a spike.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
[Access(typeof(SharedKitchenSpikeSystem))]
|
||||
public sealed partial class KitchenSpikeComponent : Component
|
||||
{
|
||||
[DataField("delay")]
|
||||
public float SpikeDelay = 7.0f;
|
||||
/// <summary>
|
||||
/// Default sound to play when the victim is hooked or unhooked.
|
||||
/// </summary>
|
||||
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpike = new("Spike");
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("sound")]
|
||||
public SoundSpecifier SpikeSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
|
||||
/// <summary>
|
||||
/// Default sound to play when the victim is butchered.
|
||||
/// </summary>
|
||||
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpikeButcher = new("SpikeButcher");
|
||||
|
||||
public List<string>? PrototypesToSpawn;
|
||||
/// <summary>
|
||||
/// ID of the container where the victim will be stored.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public string ContainerId = "body";
|
||||
|
||||
// TODO: Spiking alive mobs? (Replace with uid) (deal damage to their limbs on spiking, kill on first butcher attempt?)
|
||||
public string MeatSource1p = "?";
|
||||
public string MeatSource0 = "?";
|
||||
public string Victim = "?";
|
||||
/// <summary>
|
||||
/// Container where the victim will be stored.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public ContainerSlot BodyContainer = default!;
|
||||
|
||||
// Prevents simultaneous spiking of two bodies (could be replaced with CancellationToken, but I don't see any situation where Cancel could be called)
|
||||
public bool InUse;
|
||||
/// <summary>
|
||||
/// Sound to play when the victim is hooked or unhooked.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier SpikeSound = new SoundCollectionSpecifier(DefaultSpike);
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum KitchenSpikeVisuals : byte
|
||||
/// <summary>
|
||||
/// Sound to play when the victim is butchered.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier ButcherSound = new SoundCollectionSpecifier(DefaultSpikeButcher);
|
||||
|
||||
/// <summary>
|
||||
/// Damage that will be applied to the victim when they are hooked or unhooked.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public DamageSpecifier SpikeDamage = new()
|
||||
{
|
||||
Status
|
||||
}
|
||||
DamageDict = new Dictionary<string, FixedPoint2>
|
||||
{
|
||||
{ "Piercing", 10 },
|
||||
},
|
||||
};
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum KitchenSpikeStatus : byte
|
||||
/// <summary>
|
||||
/// Damage that will be applied to the victim when they are butchered.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public DamageSpecifier ButcherDamage = new()
|
||||
{
|
||||
Empty,
|
||||
Bloody
|
||||
}
|
||||
DamageDict = new Dictionary<string, FixedPoint2>
|
||||
{
|
||||
{ "Slash", 20 },
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Damage that the victim will receive over time.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public DamageSpecifier TimeDamage = new()
|
||||
{
|
||||
DamageDict = new Dictionary<string, FixedPoint2>
|
||||
{
|
||||
{ "Blunt", 1 }, // Mobs are only gibbed from blunt (at least for now).
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The next time when the damage will be applied to the victim.
|
||||
/// </summary>
|
||||
[AutoPausedField, AutoNetworkedField]
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextDamage;
|
||||
|
||||
/// <summary>
|
||||
/// How often the damage should be applied to the victim.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan DamageInterval = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Time that it will take to put the victim on the spike.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan HookDelay = TimeSpan.FromSeconds(7);
|
||||
|
||||
/// <summary>
|
||||
/// Time that it will take to put the victim off the spike.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan UnhookDelay = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Time that it will take to butcher the victim while they are alive.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is summed up with a <see cref="ButcherableComponent"/>'s butcher delay in butcher DoAfter.
|
||||
/// </remarks>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan ButcherDelayAlive = TimeSpan.FromSeconds(8);
|
||||
|
||||
/// <summary>
|
||||
/// Value by which the butchering delay will be multiplied if the victim is dead.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float ButcherModifierDead = 0.5f;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum KitchenSpikeVisuals : byte
|
||||
{
|
||||
Status,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum KitchenSpikeStatus : byte
|
||||
{
|
||||
Empty,
|
||||
Bloody, // TODO: Add sprites for different species.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Kitchen.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used to mark entities that are currently hooked on the spike.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedKitchenSpikeSystem))]
|
||||
public sealed partial class KitchenSpikeHookedComponent : Component;
|
||||
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Kitchen.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used to mark entity that was butchered on the spike.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedKitchenSpikeSystem))]
|
||||
public sealed partial class KitchenSpikeVictimComponent : Component;
|
||||
26
Content.Shared/Kitchen/Components/SharpComponent.cs
Normal file
26
Content.Shared/Kitchen/Components/SharpComponent.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Kitchen.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Applies to items that are capable of butchering entities, or
|
||||
/// are otherwise sharp for some purpose.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[AutoGenerateComponentState]
|
||||
public sealed partial class SharpComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// List of the entities that are currently being butchered.
|
||||
/// </summary>
|
||||
// TODO just make this a tool type. Move SharpSystem to shared.
|
||||
[AutoNetworkedField]
|
||||
public readonly HashSet<EntityUid> Butchering = [];
|
||||
|
||||
/// <summary>
|
||||
/// Affects butcher delay of the <see cref="ButcherableComponent"/>.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float ButcherDelayModifier = 1.0f;
|
||||
}
|
||||
@@ -1,38 +1,456 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Kitchen.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Kitchen;
|
||||
|
||||
public abstract class SharedKitchenSpikeSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Used to butcher some entities like monkeys.
|
||||
/// </summary>
|
||||
public sealed class SharedKitchenSpikeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _logger = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly SharedBodySystem _bodySystem = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, EntInsertedIntoContainerMessage>(OnEntInsertedIntoContainer);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, EntRemovedFromContainerMessage>(OnEntRemovedFromContainer);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, CanDropTargetEvent>(OnCanDrop);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, DragDropTargetEvent>(OnDragDrop);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, SpikeHookDoAfterEvent>(OnSpikeHookDoAfter);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, SpikeUnhookDoAfterEvent>(OnSpikeUnhookDoAfter);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, SpikeButcherDoAfterEvent>(OnSpikeButcherDoAfter);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, ExaminedEvent>(OnSpikeExamined);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
|
||||
SubscribeLocalEvent<KitchenSpikeComponent, DestructionEventArgs>(OnDestruction);
|
||||
|
||||
SubscribeLocalEvent<KitchenSpikeVictimComponent, ExaminedEvent>(OnVictimExamined);
|
||||
|
||||
// Prevent the victim from doing anything while on the spike.
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, UpdateCanMoveEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, UseAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, ThrowAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, DropAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, AttackAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, PickupAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, IsEquippingAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<KitchenSpikeHookedComponent, IsUnequippingAttemptEvent>(OnAttempt);
|
||||
}
|
||||
|
||||
private void OnCanDrop(EntityUid uid, KitchenSpikeComponent component, ref CanDropTargetEvent args)
|
||||
private void OnInit(Entity<KitchenSpikeComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
if (args.Handled)
|
||||
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent, ent.Comp.ContainerId);
|
||||
}
|
||||
|
||||
private void OnInsertAttempt(Entity<KitchenSpikeComponent> ent, ref ContainerIsInsertingAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled || TryComp<ButcherableComponent>(args.EntityUid, out var butcherable) && butcherable.Type == ButcheringType.Spike)
|
||||
return;
|
||||
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnEntInsertedIntoContainer(Entity<KitchenSpikeComponent> ent, ref EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
EnsureComp<KitchenSpikeHookedComponent>(args.Entity);
|
||||
_damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true);
|
||||
|
||||
// TODO: Add sprites for different species.
|
||||
_appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Bloody);
|
||||
}
|
||||
|
||||
private void OnEntRemovedFromContainer(Entity<KitchenSpikeComponent> ent, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
RemComp<KitchenSpikeHookedComponent>(args.Entity);
|
||||
_damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true);
|
||||
|
||||
_appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Empty);
|
||||
}
|
||||
|
||||
private void OnInteractHand(Entity<KitchenSpikeComponent> ent, ref InteractHandEvent args)
|
||||
{
|
||||
var victim = ent.Comp.BodyContainer.ContainedEntity;
|
||||
|
||||
if (args.Handled || !victim.HasValue)
|
||||
return;
|
||||
|
||||
_popupSystem.PopupClient(Loc.GetString("butcherable-need-knife",
|
||||
("target", Identity.Entity(victim.Value, EntityManager))),
|
||||
ent,
|
||||
args.User,
|
||||
PopupType.Medium);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnInteractUsing(Entity<KitchenSpikeComponent> ent, ref InteractUsingEvent args)
|
||||
{
|
||||
var victim = ent.Comp.BodyContainer.ContainedEntity;
|
||||
|
||||
if (args.Handled || !TryComp<ButcherableComponent>(victim, out var butcherable) || butcherable.SpawnedEntities.Count == 0)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (!HasComp<ButcherableComponent>(args.Dragged))
|
||||
if (!TryComp<SharpComponent>(args.Used, out var sharp))
|
||||
{
|
||||
args.CanDrop = false;
|
||||
_popupSystem.PopupClient(Loc.GetString("butcherable-need-knife",
|
||||
("target", Identity.Entity(victim.Value, EntityManager))),
|
||||
ent,
|
||||
args.User,
|
||||
PopupType.Medium);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Once we get silicons need to check organic
|
||||
args.CanDrop = true;
|
||||
var victimIdentity = Identity.Entity(victim.Value, EntityManager);
|
||||
|
||||
_popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-begin-butcher-self", ("victim", victimIdentity)),
|
||||
Loc.GetString("comp-kitchen-spike-begin-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)),
|
||||
ent,
|
||||
args.User,
|
||||
PopupType.MediumCaution);
|
||||
|
||||
var delay = TimeSpan.FromSeconds(sharp.ButcherDelayModifier * butcherable.ButcherDelay);
|
||||
|
||||
if (_mobStateSystem.IsAlive(victim.Value))
|
||||
delay += ent.Comp.ButcherDelayAlive;
|
||||
else
|
||||
delay *= ent.Comp.ButcherModifierDead;
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
|
||||
args.User,
|
||||
delay,
|
||||
new SpikeButcherDoAfterEvent(),
|
||||
ent,
|
||||
target: victim,
|
||||
used: args.Used)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnCanDrop(Entity<KitchenSpikeComponent> ent, ref CanDropTargetEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.CanDrop = _containerSystem.CanInsert(args.Dragged, ent.Comp.BodyContainer);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDragDrop(Entity<KitchenSpikeComponent> ent, ref DragDropTargetEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
ShowPopups("comp-kitchen-spike-begin-hook-self",
|
||||
"comp-kitchen-spike-begin-hook-self-other",
|
||||
"comp-kitchen-spike-begin-hook-other-self",
|
||||
"comp-kitchen-spike-begin-hook-other",
|
||||
args.User,
|
||||
args.Dragged,
|
||||
ent);
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
|
||||
args.User,
|
||||
ent.Comp.HookDelay,
|
||||
new SpikeHookDoAfterEvent(),
|
||||
ent,
|
||||
target: args.Dragged)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true,
|
||||
});
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSpikeHookDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeHookDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || !args.Target.HasValue)
|
||||
return;
|
||||
|
||||
if (_containerSystem.Insert(args.Target.Value, ent.Comp.BodyContainer))
|
||||
{
|
||||
ShowPopups("comp-kitchen-spike-hook-self",
|
||||
"comp-kitchen-spike-hook-self-other",
|
||||
"comp-kitchen-spike-hook-other-self",
|
||||
"comp-kitchen-spike-hook-other",
|
||||
args.User,
|
||||
args.Target.Value,
|
||||
ent);
|
||||
|
||||
_logger.Add(LogType.Action,
|
||||
LogImpact.High,
|
||||
$"{ToPrettyString(args.User):user} put {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
|
||||
|
||||
_audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSpikeUnhookDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeUnhookDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || !args.Target.HasValue)
|
||||
return;
|
||||
|
||||
if (_containerSystem.Remove(args.Target.Value, ent.Comp.BodyContainer))
|
||||
{
|
||||
ShowPopups("comp-kitchen-spike-unhook-self",
|
||||
"comp-kitchen-spike-unhook-self-other",
|
||||
"comp-kitchen-spike-unhook-other-self",
|
||||
"comp-kitchen-spike-unhook-other",
|
||||
args.User,
|
||||
args.Target.Value,
|
||||
ent);
|
||||
|
||||
_logger.Add(LogType.Action,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(args.User):user} took {ToPrettyString(args.Target):target} off the {ToPrettyString(ent):spike}");
|
||||
|
||||
_audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSpikeButcherDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeButcherDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || !args.Target.HasValue || !args.Used.HasValue || !TryComp<ButcherableComponent>(args.Target, out var butcherable) )
|
||||
return;
|
||||
|
||||
var victimIdentity = Identity.Entity(args.Target.Value, EntityManager);
|
||||
|
||||
_popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-butcher-self", ("victim", victimIdentity)),
|
||||
Loc.GetString("comp-kitchen-spike-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)),
|
||||
ent,
|
||||
args.User,
|
||||
PopupType.MediumCaution);
|
||||
|
||||
// Get a random entry to spawn.
|
||||
var index = _random.Next(butcherable.SpawnedEntities.Count);
|
||||
var entry = butcherable.SpawnedEntities[index];
|
||||
|
||||
var uid = PredictedSpawnNextToOrDrop(entry.PrototypeId, ent);
|
||||
_metaDataSystem.SetEntityName(uid,
|
||||
Loc.GetString("comp-kitchen-spike-meat-name",
|
||||
("name", Name(uid)),
|
||||
("victim", args.Target)));
|
||||
|
||||
// Decrease the amount since we spawned an entity from that entry.
|
||||
entry.Amount--;
|
||||
|
||||
// Remove the entry if its new amount is zero, or update it.
|
||||
if (entry.Amount <= 0)
|
||||
butcherable.SpawnedEntities.RemoveAt(index);
|
||||
else
|
||||
butcherable.SpawnedEntities[index] = entry;
|
||||
|
||||
Dirty(args.Target.Value, butcherable);
|
||||
|
||||
// Gib the victim if there is nothing else to butcher.
|
||||
if (butcherable.SpawnedEntities.Count == 0)
|
||||
{
|
||||
_bodySystem.GibBody(args.Target.Value, true);
|
||||
|
||||
_logger.Add(LogType.Gib,
|
||||
LogImpact.Extreme,
|
||||
$"{ToPrettyString(args.User):user} finished butchering {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureComp<KitchenSpikeVictimComponent>(args.Target.Value);
|
||||
|
||||
_damageableSystem.TryChangeDamage(args.Target, ent.Comp.ButcherDamage, true);
|
||||
_logger.Add(LogType.Action,
|
||||
LogImpact.Extreme,
|
||||
$"{ToPrettyString(args.User):user} butchered {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
|
||||
}
|
||||
|
||||
_audioSystem.PlayPredicted(ent.Comp.ButcherSound, ent, args.User);
|
||||
|
||||
_popupSystem.PopupClient(Loc.GetString("butcherable-knife-butchered-success",
|
||||
("target", Identity.Entity(args.Target.Value, EntityManager)),
|
||||
("knife", args.Used.Value)),
|
||||
ent,
|
||||
args.User,
|
||||
PopupType.Medium);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSpikeExamined(Entity<KitchenSpikeComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
var victim = ent.Comp.BodyContainer.ContainedEntity;
|
||||
|
||||
if (!victim.HasValue)
|
||||
return;
|
||||
|
||||
// Show it at the end of the examine so it looks good.
|
||||
args.PushMarkup(Loc.GetString("comp-kitchen-spike-hooked", ("victim", Identity.Entity(victim.Value, EntityManager))), -1);
|
||||
args.PushMessage(_examineSystem.GetExamineText(victim.Value, args.Examiner), -2);
|
||||
}
|
||||
|
||||
private void OnGetVerbs(Entity<KitchenSpikeComponent> ent, ref GetVerbsEvent<Verb> args)
|
||||
{
|
||||
var victim = ent.Comp.BodyContainer.ContainedEntity;
|
||||
|
||||
if (!victim.HasValue || !_containerSystem.CanRemove(victim.Value, ent.Comp.BodyContainer))
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
|
||||
args.Verbs.Add(new Verb()
|
||||
{
|
||||
Text = Loc.GetString("comp-kitchen-spike-unhook-verb"),
|
||||
Act = () => TryUnhook(ent, user, victim.Value),
|
||||
Impact = LogImpact.Medium,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDestruction(Entity<KitchenSpikeComponent> ent, ref DestructionEventArgs args)
|
||||
{
|
||||
_containerSystem.EmptyContainer(ent.Comp.BodyContainer, destination: Transform(ent).Coordinates);
|
||||
}
|
||||
|
||||
private void OnVictimExamined(Entity<KitchenSpikeVictimComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("comp-kitchen-spike-victim-examine", ("target", Identity.Entity(ent, EntityManager))));
|
||||
}
|
||||
|
||||
private static void OnAttempt(EntityUid uid, KitchenSpikeHookedComponent component, CancellableEntityEventArgs args)
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = AllEntityQuery<KitchenSpikeComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var kitchenSpike))
|
||||
{
|
||||
if (kitchenSpike.NextDamage > _gameTiming.CurTime)
|
||||
continue;
|
||||
|
||||
kitchenSpike.NextDamage += kitchenSpike.DamageInterval;
|
||||
Dirty(uid, kitchenSpike);
|
||||
|
||||
_damageableSystem.TryChangeDamage(kitchenSpike.BodyContainer.ContainedEntity, kitchenSpike.TimeDamage, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A helper method to show predicted popups that can be targeted towards yourself or somebody else.
|
||||
/// </summary>
|
||||
private void ShowPopups(string selfLocMessageSelf,
|
||||
string selfLocMessageOthers,
|
||||
string locMessageSelf,
|
||||
string locMessageOthers,
|
||||
EntityUid user,
|
||||
EntityUid victim,
|
||||
EntityUid hook)
|
||||
{
|
||||
string messageSelf, messageOthers;
|
||||
|
||||
var victimIdentity = Identity.Entity(victim, EntityManager);
|
||||
|
||||
if (user == victim)
|
||||
{
|
||||
messageSelf = Loc.GetString(selfLocMessageSelf, ("hook", hook));
|
||||
messageOthers = Loc.GetString(selfLocMessageOthers, ("victim", victimIdentity), ("hook", hook));
|
||||
}
|
||||
else
|
||||
{
|
||||
messageSelf = Loc.GetString(locMessageSelf, ("victim", victimIdentity), ("hook", hook));
|
||||
messageOthers = Loc.GetString(locMessageOthers,
|
||||
("user", Identity.Entity(user, EntityManager)),
|
||||
("victim", victimIdentity),
|
||||
("hook", hook));
|
||||
}
|
||||
|
||||
_popupSystem.PopupPredicted(messageSelf, messageOthers, hook, user, PopupType.MediumCaution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to unhook the victim.
|
||||
/// </summary>
|
||||
private void TryUnhook(Entity<KitchenSpikeComponent> ent, EntityUid user, EntityUid target)
|
||||
{
|
||||
ShowPopups("comp-kitchen-spike-begin-unhook-self",
|
||||
"comp-kitchen-spike-begin-unhook-self-other",
|
||||
"comp-kitchen-spike-begin-unhook-other-self",
|
||||
"comp-kitchen-spike-begin-unhook-other",
|
||||
user,
|
||||
target,
|
||||
ent);
|
||||
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
|
||||
user,
|
||||
ent.Comp.UnhookDelay,
|
||||
new SpikeUnhookDoAfterEvent(),
|
||||
ent,
|
||||
target: target)
|
||||
{
|
||||
BreakOnDamage = user != target,
|
||||
BreakOnMove = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class SpikeDoAfterEvent : SimpleDoAfterEvent
|
||||
{
|
||||
}
|
||||
public sealed partial class SpikeHookDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class SpikeUnhookDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class SpikeButcherDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
@@ -26,10 +26,14 @@ namespace Content.Shared.Lathe
|
||||
// Otherwise the material arbitrage test and/or LatheSystem.GetAllBaseRecipes needs to be updated
|
||||
|
||||
/// <summary>
|
||||
/// The lathe's construction queue
|
||||
/// The lathe's construction queue.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a LinkedList to allow for constant time insertion/deletion (vs a List), and more efficient
|
||||
/// moves (vs a Queue).
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public Queue<ProtoId<LatheRecipePrototype>> Queue = new();
|
||||
public LinkedList<LatheRecipeBatch> Queue = new();
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays when the lathe is producing an item, if any
|
||||
@@ -97,6 +101,21 @@ namespace Content.Shared.Lathe
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed partial class LatheRecipeBatch
|
||||
{
|
||||
public ProtoId<LatheRecipePrototype> Recipe;
|
||||
public int ItemsPrinted;
|
||||
public int ItemsRequested;
|
||||
|
||||
public LatheRecipeBatch(ProtoId<LatheRecipePrototype> recipe, int itemsPrinted, int itemsRequested)
|
||||
{
|
||||
Recipe = recipe;
|
||||
ItemsPrinted = itemsPrinted;
|
||||
ItemsRequested = itemsRequested;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on a lathe when it starts producing a recipe.
|
||||
/// </summary>
|
||||
|
||||
@@ -10,11 +10,11 @@ public sealed class LatheUpdateState : BoundUserInterfaceState
|
||||
{
|
||||
public List<ProtoId<LatheRecipePrototype>> Recipes;
|
||||
|
||||
public ProtoId<LatheRecipePrototype>[] Queue;
|
||||
public LatheRecipeBatch[] Queue;
|
||||
|
||||
public ProtoId<LatheRecipePrototype>? CurrentlyProducing;
|
||||
|
||||
public LatheUpdateState(List<ProtoId<LatheRecipePrototype>> recipes, ProtoId<LatheRecipePrototype>[] queue, ProtoId<LatheRecipePrototype>? currentlyProducing = null)
|
||||
public LatheUpdateState(List<ProtoId<LatheRecipePrototype>> recipes, LatheRecipeBatch[] queue, ProtoId<LatheRecipePrototype>? currentlyProducing = null)
|
||||
{
|
||||
Recipes = recipes;
|
||||
Queue = queue;
|
||||
@@ -46,6 +46,33 @@ public sealed class LatheQueueRecipeMessage : BoundUserInterfaceMessage
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent to the server to remove a batch from the queue.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class LatheDeleteRequestMessage(int index) : BoundUserInterfaceMessage
|
||||
{
|
||||
public int Index = index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent to the server to move the position of a batch in the queue.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class LatheMoveRequestMessage(int index, int change) : BoundUserInterfaceMessage
|
||||
{
|
||||
public int Index = index;
|
||||
public int Change = change;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sent to the server to stop producing the current item.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class LatheAbortFabricationMessage() : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
public enum LatheUiKey
|
||||
{
|
||||
|
||||
81
Content.Shared/Lathe/PrototypeIdLinkedListSerializer.cs
Normal file
81
Content.Shared/Lathe/PrototypeIdLinkedListSerializer.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Validation;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
||||
|
||||
namespace Content.Shared.Lathe;
|
||||
|
||||
/// <summary>
|
||||
/// Handles reading, writing, and validation for linked lists of prototypes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of prototype this linked list represents</typeparam>
|
||||
/// <remarks>
|
||||
/// This is in the Content.Shared.Lathe namespace as there are no other LinkedList ProtoId instances.
|
||||
/// </remarks>
|
||||
[TypeSerializer]
|
||||
public sealed class LinkedListSerializer<T> : ITypeSerializer<LinkedList<T>, SequenceDataNode>, ITypeCopier<LinkedList<T>> where T : class
|
||||
{
|
||||
public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node,
|
||||
IDependencyCollection dependencies, ISerializationContext? context = null)
|
||||
{
|
||||
var list = new List<ValidationNode>();
|
||||
|
||||
foreach (var elem in node.Sequence)
|
||||
{
|
||||
list.Add(serializationManager.ValidateNode<T>(elem, context));
|
||||
}
|
||||
|
||||
return new ValidatedSequenceNode(list);
|
||||
}
|
||||
|
||||
public DataNode Write(ISerializationManager serializationManager, LinkedList<T> value,
|
||||
IDependencyCollection dependencies,
|
||||
bool alwaysWrite = false,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
var sequence = new SequenceDataNode();
|
||||
|
||||
foreach (var elem in value)
|
||||
{
|
||||
sequence.Add(serializationManager.WriteValue(elem, alwaysWrite, context));
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
LinkedList<T> ITypeReader<LinkedList<T>, SequenceDataNode>.Read(ISerializationManager serializationManager,
|
||||
SequenceDataNode node,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context, ISerializationManager.InstantiationDelegate<LinkedList<T>>? instanceProvider)
|
||||
{
|
||||
var list = instanceProvider != null ? instanceProvider() : new LinkedList<T>();
|
||||
|
||||
foreach (var dataNode in node.Sequence)
|
||||
{
|
||||
list.AddLast(serializationManager.Read<T>(dataNode, hookCtx, context));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void CopyTo(
|
||||
ISerializationManager serializationManager,
|
||||
LinkedList<T> source,
|
||||
ref LinkedList<T> target,
|
||||
IDependencyCollection dependencies,
|
||||
SerializationHookContext hookCtx,
|
||||
ISerializationContext? context = null)
|
||||
{
|
||||
target.Clear();
|
||||
using var enumerator = source.GetEnumerator();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var current = enumerator.Current;
|
||||
target.AddLast(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public abstract class SharedLatheSystem : EntitySystem
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
public readonly Dictionary<string, List<LatheRecipePrototype>> InverseRecipes = new();
|
||||
public const int MaxItemsPerRequest = 10_000;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -86,6 +87,8 @@ public abstract class SharedLatheSystem : EntitySystem
|
||||
return false;
|
||||
if (!HasRecipe(uid, recipe, component))
|
||||
return false;
|
||||
if (amount <= 0)
|
||||
return false;
|
||||
|
||||
foreach (var (material, needed) in recipe.Materials)
|
||||
{
|
||||
|
||||
@@ -51,11 +51,12 @@ public sealed class LockSystem : EntitySystem
|
||||
SubscribeLocalEvent<LockComponent, LockDoAfter>(OnDoAfterLock);
|
||||
SubscribeLocalEvent<LockComponent, UnlockDoAfter>(OnDoAfterUnlock);
|
||||
SubscribeLocalEvent<LockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened); //CrystallEdge Lock System Adapt
|
||||
SubscribeLocalEvent<LockComponent, StorageInteractAttemptEvent>(OnStorageInteractAttempt);
|
||||
|
||||
|
||||
SubscribeLocalEvent<LockedWiresPanelComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
|
||||
SubscribeLocalEvent<LockedWiresPanelComponent, AttemptChangePanelEvent>(OnAttemptChangePanel);
|
||||
SubscribeLocalEvent<LockedAnchorableComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
|
||||
SubscribeLocalEvent<LockedStorageComponent, StorageInteractAttemptEvent>(OnStorageInteractAttempt);
|
||||
|
||||
SubscribeLocalEvent<UIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
|
||||
SubscribeLocalEvent<UIRequiresLockComponent, LockToggledEvent>(LockToggled);
|
||||
@@ -378,9 +379,9 @@ public sealed class LockSystem : EntitySystem
|
||||
TryUnlock(uid, args.User, skipDoAfter: true);
|
||||
}
|
||||
|
||||
private void OnStorageInteractAttempt(Entity<LockComponent> ent, ref StorageInteractAttemptEvent args)
|
||||
private void OnStorageInteractAttempt(Entity<LockedStorageComponent> ent, ref StorageInteractAttemptEvent args)
|
||||
{
|
||||
if (ent.Comp.Locked)
|
||||
if (IsLocked(ent.Owner))
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
|
||||
10
Content.Shared/Lock/LockedStorageComponent.cs
Normal file
10
Content.Shared/Lock/LockedStorageComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Lock;
|
||||
|
||||
/// <summary>
|
||||
/// Prevents using an entity's <see cref="StorageComponent"/> if its <see cref="LockComponent"/> is locked.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class LockedStorageComponent : Component;
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Charges.Components;
|
||||
using Content.Shared.Charges.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction.Events;
|
||||
@@ -29,14 +30,19 @@ public sealed class SpellbookSystem : EntitySystem
|
||||
{
|
||||
foreach (var (id, charges) in ent.Comp.SpellActions)
|
||||
{
|
||||
var spell = _actionContainer.AddAction(ent, id);
|
||||
if (spell == null)
|
||||
var action = _actionContainer.AddAction(ent, id);
|
||||
if (action is not { } spell)
|
||||
continue;
|
||||
|
||||
// Null means infinite charges.
|
||||
if (charges is { } count)
|
||||
_sharedCharges.SetCharges(spell.Value, count);
|
||||
ent.Comp.Spells.Add(spell.Value);
|
||||
{
|
||||
EnsureComp<LimitedChargesComponent>(spell, out var chargeComp);
|
||||
_sharedCharges.SetMaxCharges((spell, chargeComp), count);
|
||||
_sharedCharges.SetCharges((spell, chargeComp), count);
|
||||
}
|
||||
|
||||
ent.Comp.Spells.Add(spell);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +81,13 @@ public sealed class SpellbookSystem : EntitySystem
|
||||
foreach (var (id, charges) in ent.Comp.SpellActions)
|
||||
{
|
||||
EntityUid? actionId = null;
|
||||
if (_actions.AddAction(args.Args.User, ref actionId, id)
|
||||
&& charges is { } count) // Null means infinite charges
|
||||
_sharedCharges.SetCharges(actionId.Value, count);
|
||||
if (!_actions.AddAction(args.Args.User, ref actionId, id)
|
||||
|| charges is not { } count // Null means infinite charges
|
||||
|| !TryComp<LimitedChargesComponent>(actionId, out var chargeComp))
|
||||
continue;
|
||||
|
||||
_sharedCharges.SetMaxCharges((actionId.Value, chargeComp), count);
|
||||
_sharedCharges.SetCharges((actionId.Value, chargeComp), count);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using Content.Shared.Mech.Equipment.Components;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -48,6 +49,7 @@ public abstract partial class SharedMechSystem : EntitySystem
|
||||
SubscribeLocalEvent<MechComponent, UserActivateInWorldEvent>(RelayInteractionEvent);
|
||||
SubscribeLocalEvent<MechComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<MechComponent, DestructionEventArgs>(OnDestruction);
|
||||
SubscribeLocalEvent<MechComponent, EntityStorageIntoContainerAttemptEvent>(OnEntityStorageDump);
|
||||
SubscribeLocalEvent<MechComponent, GetAdditionalAccessEvent>(OnGetAdditionalAccess);
|
||||
SubscribeLocalEvent<MechComponent, DragDropTargetEvent>(OnDragDrop);
|
||||
SubscribeLocalEvent<MechComponent, CanDropTargetEvent>(OnCanDragDrop);
|
||||
@@ -104,6 +106,12 @@ public abstract partial class SharedMechSystem : EntitySystem
|
||||
BreakMech(uid, component);
|
||||
}
|
||||
|
||||
private void OnEntityStorageDump(Entity<MechComponent> entity, ref EntityStorageIntoContainerAttemptEvent args)
|
||||
{
|
||||
// There's no reason we should dump into /any/ of the mech's containers.
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnGetAdditionalAccess(EntityUid uid, MechComponent component, ref GetAdditionalAccessEvent args)
|
||||
{
|
||||
var pilot = component.PilotSlot.ContainedEntity;
|
||||
@@ -147,7 +155,7 @@ public abstract partial class SharedMechSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the mech, removing the user and ejecting all installed equipment.
|
||||
/// Destroys the mech, removing the user and ejecting anything contained.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="component"></param>
|
||||
@@ -237,14 +245,19 @@ public abstract partial class SharedMechSystem : EntitySystem
|
||||
/// <param name="toRemove"></param>
|
||||
/// <param name="component"></param>
|
||||
/// <param name="equipmentComponent"></param>
|
||||
/// <param name="forced">Whether or not the removal can be cancelled</param>
|
||||
/// <param name="forced">
|
||||
/// Whether or not the removal can be cancelled, and if non-mech equipment should be ejected.
|
||||
/// </param>
|
||||
public void RemoveEquipment(EntityUid uid, EntityUid toRemove, MechComponent? component = null,
|
||||
MechEquipmentComponent? equipmentComponent = null, bool forced = false)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
if (!Resolve(toRemove, ref equipmentComponent))
|
||||
// When forced, we also want to handle the possibility that the "equipment" isn't actually equipment.
|
||||
// This /shouldn't/ be possible thanks to OnEntityStorageDump, but there's been quite a few regressions
|
||||
// with entities being hardlock stuck inside mechs.
|
||||
if (!Resolve(toRemove, ref equipmentComponent) && !forced)
|
||||
return;
|
||||
|
||||
if (!forced)
|
||||
@@ -261,7 +274,9 @@ public abstract partial class SharedMechSystem : EntitySystem
|
||||
if (component.CurrentSelectedEquipment == toRemove)
|
||||
CycleEquipment(uid, component);
|
||||
|
||||
equipmentComponent.EquipmentOwner = null;
|
||||
if (forced && equipmentComponent != null)
|
||||
equipmentComponent.EquipmentOwner = null;
|
||||
|
||||
_container.Remove(toRemove, component.EquipmentContainer);
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed partial class HealingComponent : Component
|
||||
/// How long it takes to apply the damage.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float Delay = 3f;
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(3f);
|
||||
|
||||
/// <summary>
|
||||
/// Delay multiplier when healing yourself.
|
||||
|
||||
@@ -112,9 +112,17 @@ public sealed class HealingSystem : EntitySystem
|
||||
|
||||
// Logic to determine the whether or not to repeat the healing action
|
||||
args.Repeat = HasDamage((args.Used.Value, healing), target) && !dontRepeat;
|
||||
if (!args.Repeat && !dontRepeat)
|
||||
_popupSystem.PopupClient(Loc.GetString("medical-item-finished-using", ("item", args.Used)), target.Owner, args.User);
|
||||
args.Handled = true;
|
||||
|
||||
if (!args.Repeat)
|
||||
{
|
||||
_popupSystem.PopupClient(Loc.GetString("medical-item-finished-using", ("item", args.Used)), target.Owner, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update our self heal delay so it shortens as we heal more damage.
|
||||
if (args.User == target.Owner)
|
||||
args.Args.Delay = healing.Delay * GetScaledHealingPenalty(target.Owner, healing.SelfHealPenaltyMultiplier);
|
||||
}
|
||||
|
||||
private bool HasDamage(Entity<HealingComponent> healing, Entity<DamageableComponent> target)
|
||||
@@ -203,7 +211,7 @@ public sealed class HealingSystem : EntitySystem
|
||||
|
||||
var delay = isNotSelf
|
||||
? healing.Comp.Delay
|
||||
: healing.Comp.Delay * GetScaledHealingPenalty(healing);
|
||||
: healing.Comp.Delay * GetScaledHealingPenalty(target, healing.Comp.SelfHealPenaltyMultiplier);
|
||||
|
||||
var doAfterEventArgs =
|
||||
new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), target, target: target, used: healing)
|
||||
@@ -222,21 +230,21 @@ public sealed class HealingSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Scales the self-heal penalty based on the amount of damage taken
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="component"></param>
|
||||
/// <returns></returns>
|
||||
public float GetScaledHealingPenalty(Entity<HealingComponent> healing)
|
||||
/// <param name="ent">Entity we're healing</param>
|
||||
/// <param name="mod">Maximum modifier we can have.</param>
|
||||
/// <returns>Modifier we multiply our healing time by</returns>
|
||||
public float GetScaledHealingPenalty(Entity<DamageableComponent?, MobThresholdsComponent?> ent, float mod)
|
||||
{
|
||||
var output = healing.Comp.Delay;
|
||||
if (!TryComp<MobThresholdsComponent>(healing, out var mobThreshold) ||
|
||||
!TryComp<DamageableComponent>(healing, out var damageable))
|
||||
return output;
|
||||
if (!_mobThresholdSystem.TryGetThresholdForState(healing, MobState.Critical, out var amount, mobThreshold))
|
||||
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
|
||||
return mod;
|
||||
|
||||
if (!_mobThresholdSystem.TryGetThresholdForState(ent, MobState.Critical, out var amount, ent.Comp2))
|
||||
return 1;
|
||||
|
||||
var percentDamage = (float)(damageable.TotalDamage / amount);
|
||||
var percentDamage = (float)(ent.Comp1.TotalDamage / amount);
|
||||
//basically make it scale from 1 to the multiplier.
|
||||
var modifier = percentDamage * (healing.Comp.SelfHealPenaltyMultiplier - 1) + 1;
|
||||
return Math.Max(modifier, 1);
|
||||
|
||||
var output = percentDamage * (mod - 1) + 1;
|
||||
return Math.Max(output, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Mind;
|
||||
@@ -100,10 +99,16 @@ public sealed partial class MindComponent : Component
|
||||
public bool PreventSuicide { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mind Role Entities belonging to this Mind
|
||||
/// Mind Role Entities belonging to this Mind are stored in this container.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public List<EntityUid> MindRoles = new List<EntityUid>();
|
||||
[ViewVariables]
|
||||
public Container MindRoleContainer = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The id for the MindRoleContainer.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public const string MindRoleContainerId = "mind_roles";
|
||||
|
||||
/// <summary>
|
||||
/// The mind's current antagonist/special role, or lack thereof;
|
||||
|
||||
@@ -23,7 +23,7 @@ public abstract partial class SharedMindSystem : EntitySystem
|
||||
{
|
||||
RaiseLocalEvent(mindId, ref ev);
|
||||
|
||||
foreach (var role in mindComp.MindRoles)
|
||||
foreach (var role in mindComp.MindRoleContainer.ContainedEntities)
|
||||
RaiseLocalEvent(role, ref ev);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public abstract partial class SharedMindSystem : EntitySystem
|
||||
{
|
||||
RaiseLocalEvent(mindId, ref ev);
|
||||
|
||||
foreach (var role in mindComp.MindRoles)
|
||||
foreach (var role in mindComp.MindRoleContainer.ContainedEntities)
|
||||
RaiseLocalEvent(role, ref ev);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Speech;
|
||||
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
@@ -36,6 +36,7 @@ public abstract partial class SharedMindSystem : EntitySystem
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
|
||||
[ViewVariables]
|
||||
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
|
||||
@@ -64,6 +65,8 @@ public abstract partial class SharedMindSystem : EntitySystem
|
||||
|
||||
private void OnMindStartup(EntityUid uid, MindComponent component, ComponentStartup args)
|
||||
{
|
||||
component.MindRoleContainer = _container.EnsureContainer<Container>(uid, MindComponent.MindRoleContainerId);
|
||||
|
||||
if (component.UserId == null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Mindshield.FakeMindShield;
|
||||
|
||||
public sealed class SharedFakeMindShieldSystem : EntitySystem
|
||||
public sealed class FakeMindShieldSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
@@ -24,9 +24,11 @@ public sealed class SharedFakeMindShieldSystem : EntitySystem
|
||||
SubscribeLocalEvent<FakeMindShieldComponent, ChameleonControllerOutfitSelectedEvent>(OnChameleonControllerOutfitSelected);
|
||||
}
|
||||
|
||||
private void OnToggleMindshield(EntityUid uid, FakeMindShieldComponent comp, FakeMindShieldToggleEvent toggleEvent)
|
||||
private void OnToggleMindshield(EntityUid uid, FakeMindShieldComponent comp, FakeMindShieldToggleEvent args)
|
||||
{
|
||||
comp.IsEnabled = !comp.IsEnabled;
|
||||
args.Toggle = true;
|
||||
args.Handled = true;
|
||||
Dirty(uid, comp);
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Implants;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Mindshield.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Shared.Mindshield.FakeMindShield;
|
||||
|
||||
public sealed class SharedFakeMindShieldImplantSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SubdermalImplantComponent, FakeMindShieldToggleEvent>(OnFakeMindShieldToggle);
|
||||
SubscribeLocalEvent<FakeMindShieldImplantComponent, ImplantImplantedEvent>(ImplantCheck);
|
||||
SubscribeLocalEvent<FakeMindShieldImplantComponent, EntGotRemovedFromContainerMessage>(ImplantDraw);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raise the Action of a Implanted user toggling their implant to the FakeMindshieldComponent on their entity
|
||||
/// </summary>
|
||||
private void OnFakeMindShieldToggle(Entity<SubdermalImplantComponent> entity, ref FakeMindShieldToggleEvent ev)
|
||||
{
|
||||
ev.Handled = true;
|
||||
if (entity.Comp.ImplantedEntity is not { } ent)
|
||||
return;
|
||||
|
||||
if (!TryComp<FakeMindShieldComponent>(ent, out var comp))
|
||||
return;
|
||||
// TODO: is there a reason this cant set ev.Toggle = true;
|
||||
_actionsSystem.SetToggled((ev.Action, ev.Action), !comp.IsEnabled); // Set it to what the Mindshield component WILL be after this
|
||||
RaiseLocalEvent(ent, ev); //this reraises the action event to support an eventual future Changeling Antag which will also be using this component for it's "mindshield" ability
|
||||
}
|
||||
private void ImplantCheck(EntityUid uid, FakeMindShieldImplantComponent component ,ref ImplantImplantedEvent ev)
|
||||
{
|
||||
if (ev.Implanted != null)
|
||||
EnsureComp<FakeMindShieldComponent>(ev.Implanted.Value);
|
||||
}
|
||||
|
||||
private void ImplantDraw(Entity<FakeMindShieldImplantComponent> ent, ref EntGotRemovedFromContainerMessage ev)
|
||||
{
|
||||
RemComp<FakeMindShieldComponent>(ev.Container.Owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Morgue.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used to track actively cooking crematoriums.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class ActiveCrematoriumComponent : Component;
|
||||
42
Content.Shared/Morgue/Components/CrematoriumComponent.cs
Normal file
42
Content.Shared/Morgue/Components/CrematoriumComponent.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Morgue.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Allows an entity storage to dispose bodies by turning them into ash.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
public sealed partial class CrematoriumComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity to spawn when something was burned.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntProtoId LeftOverProtoId = "Ash";
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to cremate something.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan CookTime = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp at which cremating is finished.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[AutoNetworkedField, AutoPausedField]
|
||||
public TimeSpan ActiveUntil = TimeSpan.Zero;
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier CremateStartSound = new SoundPathSpecifier("/Audio/Items/Lighters/lighter1.ogg");
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier CrematingSound = new SoundPathSpecifier("/Audio/Effects/burning.ogg");
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier CremateFinishSound = new SoundPathSpecifier("/Audio/Machines/ding.ogg");
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Morgue.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class EntityStorageLayingDownOverrideComponent : Component
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Makes an entity storage only accept entities that are laying down.
|
||||
/// This is true for mobs that are crit, dead or crawling.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EntityStorageLayingDownOverrideComponent : Component;
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Morgue.Components;
|
||||
|
||||
/// <summary>
|
||||
/// When added to an entity storage this component will keep track of the mind status of the player inside.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
public sealed partial class MorgueComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the morgue beeps if a living player is inside.
|
||||
/// Whether or not the morgue beeps if a living player is inside.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool DoSoulBeep = true;
|
||||
|
||||
[DataField]
|
||||
public float AccumulatedFrameTime = 0f;
|
||||
/// <summary>
|
||||
/// The timestamp for the next beep.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[AutoPausedField]
|
||||
public TimeSpan NextBeep = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time between each beep.
|
||||
/// The amount of time between each beep.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BeepTime = 10f;
|
||||
public TimeSpan BeepTime = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// The beep sound to play.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier OccupantHasSoulAlarmSound = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class EntityStorageLayingDownOverrideSystem : EntitySystem
|
||||
{
|
||||
// Explicitly check for standing state component, as entities without it will return false for IsDown()
|
||||
// which prevents inserting any kind of non-mobs into this container (which is unintended)
|
||||
if (TryComp<StandingStateComponent>(ent, out var standingState) && !_standing.IsDown(ent, standingState))
|
||||
if (TryComp<StandingStateComponent>(ent, out var standingState) && !_standing.IsDown((ent, standingState)))
|
||||
args.Contents.Remove(ent);
|
||||
}
|
||||
}
|
||||
|
||||
170
Content.Shared/Morgue/SharedCrematoriumSystem.cs
Normal file
170
Content.Shared/Morgue/SharedCrematoriumSystem.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Morgue.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Morgue;
|
||||
|
||||
public abstract class SharedCrematoriumSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!;
|
||||
[Dependency] protected readonly SharedPopupSystem Popup = default!;
|
||||
[Dependency] protected readonly StandingStateSystem Standing = default!;
|
||||
[Dependency] protected readonly SharedMindSystem Mind = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CrematoriumComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<CrematoriumComponent, GetVerbsEvent<AlternativeVerb>>(AddCremateVerb);
|
||||
SubscribeLocalEvent<ActiveCrematoriumComponent, StorageOpenAttemptEvent>(OnAttemptOpen);
|
||||
}
|
||||
|
||||
private void OnExamine(Entity<CrematoriumComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!TryComp<AppearanceComponent>(ent, out var appearance))
|
||||
return;
|
||||
|
||||
using (args.PushGroup(nameof(CrematoriumComponent)))
|
||||
{
|
||||
if (_appearance.TryGetData<bool>(ent.Owner, CrematoriumVisuals.Burning, out var isBurning, appearance) &&
|
||||
isBurning)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("crematorium-entity-storage-component-on-examine-details-is-burning",
|
||||
("owner", ent.Owner)));
|
||||
}
|
||||
|
||||
if (_appearance.TryGetData<bool>(ent.Owner, StorageVisuals.HasContents, out var hasContents, appearance) &&
|
||||
hasContents)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("crematorium-entity-storage-component-on-examine-details-has-contents"));
|
||||
}
|
||||
else
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("crematorium-entity-storage-component-on-examine-details-empty"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAttemptOpen(Entity<ActiveCrematoriumComponent> ent, ref StorageOpenAttemptEvent args)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void AddCremateVerb(EntityUid uid, CrematoriumComponent component, GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!TryComp<EntityStorageComponent>(uid, out var storage))
|
||||
return;
|
||||
|
||||
if (!args.CanAccess || !args.CanInteract || args.Hands == null || storage.Open)
|
||||
return;
|
||||
|
||||
if (HasComp<ActiveCrematoriumComponent>(uid))
|
||||
return;
|
||||
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Text = Loc.GetString("cremate-verb-get-data-text"),
|
||||
// TODO VERB ICON add flame/burn symbol?
|
||||
Act = () => TryCremate((uid, component, storage), args.User),
|
||||
Impact = LogImpact.High // could be a body? or evidence? I dunno.
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the cremation.
|
||||
/// </summary>
|
||||
public bool Cremate(Entity<CrematoriumComponent?> ent, EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
if (HasComp<ActiveCrematoriumComponent>(ent))
|
||||
return false;
|
||||
|
||||
_audio.PlayPredicted(ent.Comp.CremateStartSound, ent.Owner, user);
|
||||
_audio.PlayPredicted(ent.Comp.CrematingSound, ent.Owner, user);
|
||||
_appearance.SetData(ent.Owner, CrematoriumVisuals.Burning, true);
|
||||
|
||||
AddComp<ActiveCrematoriumComponent>(ent);
|
||||
ent.Comp.ActiveUntil = _timing.CurTime + ent.Comp.CookTime;
|
||||
Dirty(ent);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to start to start the cremation.
|
||||
/// Only works when the crematorium is closed and there are entities inside.
|
||||
/// </summary>
|
||||
public bool TryCremate(Entity<CrematoriumComponent?, EntityStorageComponent?> ent, EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2))
|
||||
return false;
|
||||
|
||||
if (ent.Comp2.Open || ent.Comp2.Contents.ContainedEntities.Count < 1)
|
||||
return false;
|
||||
|
||||
return Cremate((ent.Owner, ent.Comp1), user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finish the cremation process.
|
||||
/// This will delete the entities inside and spawn ash.
|
||||
/// </summary>
|
||||
private void FinishCooking(Entity<CrematoriumComponent?, EntityStorageComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2))
|
||||
return;
|
||||
|
||||
_appearance.SetData(ent.Owner, CrematoriumVisuals.Burning, false);
|
||||
RemComp<ActiveCrematoriumComponent>(ent);
|
||||
|
||||
if (ent.Comp2.Contents.ContainedEntities.Count > 0)
|
||||
{
|
||||
for (var i = ent.Comp2.Contents.ContainedEntities.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var item = ent.Comp2.Contents.ContainedEntities[i];
|
||||
_container.Remove(item, ent.Comp2.Contents);
|
||||
PredictedDel(item);
|
||||
}
|
||||
PredictedTrySpawnInContainer(ent.Comp1.LeftOverProtoId, ent.Owner, ent.Comp2.Contents.ID, out _);
|
||||
}
|
||||
|
||||
EntityStorage.OpenStorage(ent.Owner, ent.Comp2);
|
||||
|
||||
if (_net.IsServer) // can't predict without the user
|
||||
_audio.PlayPvs(ent.Comp1.CremateFinishSound, ent.Owner);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
var query = EntityQueryEnumerator<ActiveCrematoriumComponent, CrematoriumComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var crematorium))
|
||||
{
|
||||
if (curTime < crematorium.ActiveUntil)
|
||||
continue;
|
||||
|
||||
FinishCooking((uid, crematorium, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
83
Content.Shared/Morgue/SharedMorgueSystem.cs
Normal file
83
Content.Shared/Morgue/SharedMorgueSystem.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Morgue.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Shared.Morgue;
|
||||
|
||||
public abstract class SharedMorgueSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MorgueComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<MorgueComponent, StorageAfterCloseEvent>(OnClosed);
|
||||
SubscribeLocalEvent<MorgueComponent, StorageAfterOpenEvent>(OnOpened);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the examination text for looking at a morgue.
|
||||
/// </summary>
|
||||
private void OnExamine(Entity<MorgueComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
_appearance.TryGetData<MorgueContents>(ent.Owner, MorgueVisuals.Contents, out var contents);
|
||||
|
||||
var text = contents switch
|
||||
{
|
||||
MorgueContents.HasSoul => "morgue-entity-storage-component-on-examine-details-body-has-soul",
|
||||
MorgueContents.HasContents => "morgue-entity-storage-component-on-examine-details-has-contents",
|
||||
MorgueContents.HasMob => "morgue-entity-storage-component-on-examine-details-body-has-no-soul",
|
||||
_ => "morgue-entity-storage-component-on-examine-details-empty"
|
||||
};
|
||||
|
||||
args.PushMarkup(Loc.GetString(text));
|
||||
}
|
||||
|
||||
private void OnClosed(Entity<MorgueComponent> ent, ref StorageAfterCloseEvent args)
|
||||
{
|
||||
CheckContents(ent.Owner, ent.Comp);
|
||||
}
|
||||
|
||||
private void OnOpened(Entity<MorgueComponent> ent, ref StorageAfterOpenEvent args)
|
||||
{
|
||||
CheckContents(ent.Owner, ent.Comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates data in case something died/got deleted in the morgue.
|
||||
/// </summary>
|
||||
public void CheckContents(EntityUid uid, MorgueComponent? morgue = null, EntityStorageComponent? storage = null, AppearanceComponent? app = null)
|
||||
{
|
||||
if (!Resolve(uid, ref morgue, ref storage, ref app))
|
||||
return;
|
||||
|
||||
if (storage.Contents.ContainedEntities.Count == 0)
|
||||
{
|
||||
_appearance.SetData(uid, MorgueVisuals.Contents, MorgueContents.Empty, app);
|
||||
return;
|
||||
}
|
||||
|
||||
var hasMob = false;
|
||||
|
||||
foreach (var ent in storage.Contents.ContainedEntities)
|
||||
{
|
||||
if (!hasMob && HasComp<MobStateComponent>(ent))
|
||||
hasMob = true;
|
||||
|
||||
if (HasComp<ActorComponent>(ent))
|
||||
{
|
||||
_appearance.SetData(uid, MorgueVisuals.Contents, MorgueContents.HasSoul, app);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_appearance.SetData(uid, MorgueVisuals.Contents, hasMob ? MorgueContents.HasMob : MorgueContents.HasContents, app);
|
||||
}
|
||||
}
|
||||
16
Content.Shared/Movement/Components/ActiveLeaperComponent.cs
Normal file
16
Content.Shared/Movement/Components/ActiveLeaperComponent.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Movement.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Marker component given to the users of the <see cref="JumpAbilityComponent"/> if they are meant to collide with environment.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class ActiveLeaperComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The duration to stun the owner on collide with environment.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan KnockdownDuration;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Content.Shared.Actions;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Movement.Components;
|
||||
|
||||
@@ -13,6 +14,18 @@ namespace Content.Shared.Movement.Components;
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedJumpAbilitySystem))]
|
||||
public sealed partial class JumpAbilityComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The action prototype that allows you to jump.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId Action = "ActionGravityJump";
|
||||
|
||||
/// <summary>
|
||||
/// Entity to hold the action prototype.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? ActionEntity;
|
||||
|
||||
/// <summary>
|
||||
/// How far you will jump (in tiles).
|
||||
/// </summary>
|
||||
@@ -25,11 +38,29 @@ public sealed partial class JumpAbilityComponent : Component
|
||||
[DataField, AutoNetworkedField]
|
||||
public float JumpThrowSpeed = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this entity can collide with another entity, leading to it getting knocked down.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool CanCollide = false;
|
||||
|
||||
/// <summary>
|
||||
/// The duration of the knockdown in case of a collision from CanCollide.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan CollideKnockdown = TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <summary>
|
||||
/// This gets played whenever the jump action is used.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public SoundSpecifier? JumpSound;
|
||||
|
||||
/// <summary>
|
||||
/// The popup to show if the entity is unable to perform a jump.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public LocId? JumpFailedPopup = "jump-ability-failure";
|
||||
}
|
||||
|
||||
public sealed partial class GravityJumpEvent : InstantActionEvent;
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Movement.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Ignores gravity entirely.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class MovementIgnoreGravityComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not gravity is on or off for this object.
|
||||
/// Whether gravity is on or off for this object. This will always override the current Gravity State.
|
||||
/// </summary>
|
||||
[DataField("gravityState")] public bool Weightless = false;
|
||||
}
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
public sealed class MovementIgnoreGravityComponentState : ComponentState
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Weightless;
|
||||
|
||||
public MovementIgnoreGravityComponentState(MovementIgnoreGravityComponent component)
|
||||
{
|
||||
Weightless = component.Weightless;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user