diff --git a/Content.Client/Kitchen/KitchenSpikeSystem.cs b/Content.Client/Kitchen/KitchenSpikeSystem.cs deleted file mode 100644 index 3627a29fa9..0000000000 --- a/Content.Client/Kitchen/KitchenSpikeSystem.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Content.Shared.Kitchen; - -namespace Content.Client.Kitchen; - -public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem -{ - -} diff --git a/Content.Server/Botany/Systems/BotanySystem.Seed.cs b/Content.Server/Botany/Systems/BotanySystem.Seed.cs index fd65c141aa..6b26ce7119 100644 --- a/Content.Server/Botany/Systems/BotanySystem.Seed.cs +++ b/Content.Server/Botany/Systems/BotanySystem.Seed.cs @@ -1,5 +1,4 @@ using Content.Server.Botany.Components; -using Content.Server.Kitchen.Components; using Content.Server.Popups; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Botany; @@ -16,6 +15,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Database; +using Content.Shared.Kitchen.Components; namespace Content.Server.Botany.Systems; diff --git a/Content.Server/Botany/Systems/LogSystem.cs b/Content.Server/Botany/Systems/LogSystem.cs index 3d415635be..08190af708 100644 --- a/Content.Server/Botany/Systems/LogSystem.cs +++ b/Content.Server/Botany/Systems/LogSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Botany.Components; -using Content.Server.Kitchen.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; +using Content.Shared.Kitchen.Components; using Content.Shared.Random; using Robust.Shared.Containers; diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs index d8381b2a79..e38c742fa2 100644 --- a/Content.Server/Botany/Systems/PlantHolderSystem.cs +++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Botany.Components; using Content.Server.Hands.Systems; -using Content.Server.Kitchen.Components; using Content.Server.Popups; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Atmos; @@ -26,6 +25,7 @@ using Robust.Shared.Timing; using Content.Shared.Administration.Logs; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; +using Content.Shared.Kitchen.Components; using Content.Shared.Labels.Components; namespace Content.Server.Botany.Systems; diff --git a/Content.Server/Kitchen/Components/SharpComponent.cs b/Content.Server/Kitchen/Components/SharpComponent.cs deleted file mode 100644 index c67c3b8a4d..0000000000 --- a/Content.Server/Kitchen/Components/SharpComponent.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Content.Server.Kitchen.Components; - -/// -/// Applies to items that are capable of butchering entities, or -/// are otherwise sharp for some purpose. -/// -[RegisterComponent] -public sealed partial class SharpComponent : Component -{ - // TODO just make this a tool type. - public HashSet Butchering = new(); - - [DataField("butcherDelayModifier")] - public float ButcherDelayModifier = 1.0f; -} diff --git a/Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs b/Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs deleted file mode 100644 index 4ed05a3f16..0000000000 --- a/Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs +++ /dev/null @@ -1,292 +0,0 @@ -using Content.Server.Administration.Logs; -using Content.Server.Body.Systems; -using Content.Server.Kitchen.Components; -using Content.Server.Popups; -using Content.Shared.Chat; -using Content.Shared.Damage; -using Content.Shared.Database; -using Content.Shared.DoAfter; -using Content.Shared.DragDrop; -using Content.Shared.Humanoid; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; -using Content.Shared.Kitchen; -using Content.Shared.Kitchen.Components; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Nutrition.Components; -using Content.Shared.Popups; -using Content.Shared.Storage; -using Robust.Server.GameObjects; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Player; -using Robust.Shared.Random; -using static Content.Shared.Kitchen.Components.KitchenSpikeComponent; - -namespace Content.Server.Kitchen.EntitySystems -{ - public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem - { - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly IAdminLogManager _logger = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly TransformSystem _transform = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - [Dependency] private readonly SharedSuicideSystem _suicide = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnInteractHand); - SubscribeLocalEvent(OnDragDrop); - - //DoAfter - SubscribeLocalEvent(OnDoAfter); - - SubscribeLocalEvent(OnSuicideByEnvironment); - - SubscribeLocalEvent(OnButcherableCanDrop); - } - - private void OnButcherableCanDrop(Entity entity, ref CanDropDraggedEvent args) - { - args.Handled = true; - args.CanDrop |= entity.Comp.Type != ButcheringType.Knife; - } - - /// - /// TODO: Update this so it actually meatspikes the user instead of applying lethal damage to them. - /// - private void OnSuicideByEnvironment(Entity entity, ref SuicideByEnvironmentEvent args) - { - if (args.Handled) - return; - - if (!TryComp(args.Victim, out var damageableComponent)) - return; - - _suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Piercing"); - var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other", - ("victim", Identity.Entity(args.Victim, EntityManager)), - ("this", entity)); - _popupSystem.PopupEntity(othersMessage, args.Victim, Filter.PvsExcept(args.Victim), true); - - var selfMessage = Loc.GetString("comp-kitchen-spike-suicide-self", - ("this", entity)); - _popupSystem.PopupEntity(selfMessage, args.Victim, args.Victim); - args.Handled = true; - } - - private void OnDoAfter(Entity entity, ref SpikeDoAfterEvent args) - { - if (args.Args.Target == null) - return; - - if (TryComp(args.Args.Target.Value, out var butcherable)) - butcherable.BeingButchered = false; - - if (args.Cancelled) - { - entity.Comp.InUse = false; - return; - } - - if (args.Handled) - return; - - if (Spikeable(entity, args.Args.User, args.Args.Target.Value, entity.Comp, butcherable)) - Spike(entity, args.Args.User, args.Args.Target.Value, entity.Comp); - - entity.Comp.InUse = false; - args.Handled = true; - } - - private void OnDragDrop(Entity entity, ref DragDropTargetEvent args) - { - if (args.Handled) - return; - - args.Handled = true; - - if (Spikeable(entity, args.User, args.Dragged, entity.Comp)) - TrySpike(entity, args.User, args.Dragged, entity.Comp); - } - - private void OnInteractHand(Entity entity, ref InteractHandEvent args) - { - if (args.Handled) - return; - - if (entity.Comp.PrototypesToSpawn?.Count > 0) - { - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), entity, args.User); - args.Handled = true; - } - } - - private void OnInteractUsing(Entity entity, ref InteractUsingEvent args) - { - if (args.Handled) - return; - - if (TryGetPiece(entity, args.User, args.Used)) - args.Handled = true; - } - - private void Spike(EntityUid uid, EntityUid userUid, EntityUid victimUid, - KitchenSpikeComponent? component = null, ButcherableComponent? butcherable = null) - { - if (!Resolve(uid, ref component) || !Resolve(victimUid, ref butcherable)) - return; - - var logImpact = LogImpact.Medium; - if (HasComp(victimUid)) - logImpact = LogImpact.Extreme; - - _logger.Add(LogType.Gib, logImpact, $"{ToPrettyString(userUid):user} kitchen spiked {ToPrettyString(victimUid):target}"); - - // TODO VERY SUS - component.PrototypesToSpawn = EntitySpawnCollection.GetSpawns(butcherable.SpawnedEntities, _random); - - // This feels not okay, but entity is getting deleted on "Spike", for now... - component.MeatSource1p = Loc.GetString("comp-kitchen-spike-remove-meat", ("victim", victimUid)); - component.MeatSource0 = Loc.GetString("comp-kitchen-spike-remove-meat-last", ("victim", victimUid)); - component.Victim = Name(victimUid); - - UpdateAppearance(uid, null, component); - - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-kill", - ("user", Identity.Entity(userUid, EntityManager)), - ("victim", Identity.Entity(victimUid, EntityManager)), - ("this", uid)), - uid, PopupType.LargeCaution); - - _transform.SetCoordinates(victimUid, Transform(uid).Coordinates); - // THE WHAT? - // TODO: Need to be able to leave them on the spike to do DoT, see ss13. - var gibs = _bodySystem.GibBody(victimUid); - foreach (var gib in gibs) { - QueueDel(gib); - } - - _audio.PlayPvs(component.SpikeSound, uid); - } - - private bool TryGetPiece(EntityUid uid, EntityUid user, EntityUid used, - KitchenSpikeComponent? component = null, SharpComponent? sharp = null) - { - if (!Resolve(uid, ref component) || component.PrototypesToSpawn == null || component.PrototypesToSpawn.Count == 0) - return false; - - // Is using knife - if (!Resolve(used, ref sharp, false) ) - { - return false; - } - - var item = _random.PickAndTake(component.PrototypesToSpawn); - - var ent = Spawn(item, Transform(uid).Coordinates); - _metaData.SetEntityName(ent, - Loc.GetString("comp-kitchen-spike-meat-name", ("name", Name(ent)), ("victim", component.Victim))); - - if (component.PrototypesToSpawn.Count != 0) - _popupSystem.PopupEntity(component.MeatSource1p, uid, user, PopupType.MediumCaution); - else - { - UpdateAppearance(uid, null, component); - _popupSystem.PopupEntity(component.MeatSource0, uid, user, PopupType.MediumCaution); - } - - return true; - } - - private void UpdateAppearance(EntityUid uid, AppearanceComponent? appearance = null, KitchenSpikeComponent? component = null) - { - if (!Resolve(uid, ref component, ref appearance, false)) - return; - - _appearance.SetData(uid, KitchenSpikeVisuals.Status, component.PrototypesToSpawn?.Count > 0 ? KitchenSpikeStatus.Bloody : KitchenSpikeStatus.Empty, appearance); - } - - private bool Spikeable(EntityUid uid, EntityUid userUid, EntityUid victimUid, - KitchenSpikeComponent? component = null, ButcherableComponent? butcherable = null) - { - if (!Resolve(uid, ref component)) - return false; - - if (component.PrototypesToSpawn?.Count > 0) - { - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-collect", ("this", uid)), uid, userUid); - return false; - } - - if (!Resolve(victimUid, ref butcherable, false)) - { - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-butcher", ("victim", Identity.Entity(victimUid, EntityManager)), ("this", uid)), victimUid, userUid); - return false; - } - - switch (butcherable.Type) - { - case ButcheringType.Spike: - return true; - case ButcheringType.Knife: - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-butcher-knife", ("victim", Identity.Entity(victimUid, EntityManager)), ("this", uid)), victimUid, userUid); - return false; - default: - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-butcher", ("victim", Identity.Entity(victimUid, EntityManager)), ("this", uid)), victimUid, userUid); - return false; - } - } - - public bool TrySpike(EntityUid uid, EntityUid userUid, EntityUid victimUid, KitchenSpikeComponent? component = null, - ButcherableComponent? butcherable = null, MobStateComponent? mobState = null) - { - if (!Resolve(uid, ref component) || component.InUse || - !Resolve(victimUid, ref butcherable) || butcherable.BeingButchered) - return false; - - // THE WHAT? (again) - // Prevent dead from being spiked TODO: Maybe remove when rounds can be played and DOT is implemented - if (Resolve(victimUid, ref mobState, false) && - _mobStateSystem.IsAlive(victimUid, mobState)) - { - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-not-dead", ("victim", Identity.Entity(victimUid, EntityManager))), - victimUid, userUid); - return true; - } - - if (userUid != victimUid) - { - _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-begin-hook-victim", ("user", Identity.Entity(userUid, EntityManager)), ("this", uid)), victimUid, victimUid, PopupType.LargeCaution); - } - // TODO: make it work when SuicideEvent is implemented - // else - // _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-begin-hook-self", ("this", uid)), victimUid, Filter.Pvs(uid)); // This is actually unreachable and should be in SuicideEvent - - butcherable.BeingButchered = true; - component.InUse = true; - - var doAfterArgs = new DoAfterArgs(EntityManager, userUid, component.SpikeDelay + butcherable.ButcherDelay, new SpikeDoAfterEvent(), uid, target: victimUid, used: uid) - { - BreakOnDamage = true, - BreakOnMove = true, - NeedHand = true, - BreakOnDropItem = false, - }; - - _doAfter.TryStartDoAfter(doAfterArgs); - - return true; - } - } -} diff --git a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs index 0275e4d1a7..ab6e1db494 100644 --- a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs @@ -1,5 +1,4 @@ using Content.Server.Body.Systems; -using Content.Server.Kitchen.Components; using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; using Content.Shared.Database; @@ -8,6 +7,7 @@ using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Kitchen; +using Content.Shared.Kitchen.Components; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; diff --git a/Content.Shared/Kitchen/Components/KitchenSpikeComponent.cs b/Content.Shared/Kitchen/Components/KitchenSpikeComponent.cs index 3057a75a4c..b4fdc5ed3c 100644 --- a/Content.Shared/Kitchen/Components/KitchenSpikeComponent.cs +++ b/Content.Shared/Kitchen/Components/KitchenSpikeComponent.cs @@ -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; +/// +/// Used to mark entity that should act as a spike. +/// [RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState, AutoGenerateComponentPause] [Access(typeof(SharedKitchenSpikeSystem))] public sealed partial class KitchenSpikeComponent : Component { - [DataField("delay")] - public float SpikeDelay = 7.0f; + /// + /// Default sound to play when the victim is hooked or unhooked. + /// + private static readonly ProtoId DefaultSpike = new("Spike"); - [ViewVariables(VVAccess.ReadWrite)] - [DataField("sound")] - public SoundSpecifier SpikeSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg"); + /// + /// Default sound to play when the victim is butchered. + /// + private static readonly ProtoId DefaultSpikeButcher = new("SpikeButcher"); - public List? PrototypesToSpawn; + /// + /// ID of the container where the victim will be stored. + /// + [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 = "?"; + /// + /// Container where the victim will be stored. + /// + [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; + /// + /// Sound to play when the victim is hooked or unhooked. + /// + [DataField, AutoNetworkedField] + public SoundSpecifier SpikeSound = new SoundCollectionSpecifier(DefaultSpike); - [Serializable, NetSerializable] - public enum KitchenSpikeVisuals : byte + /// + /// Sound to play when the victim is butchered. + /// + [DataField, AutoNetworkedField] + public SoundSpecifier ButcherSound = new SoundCollectionSpecifier(DefaultSpikeButcher); + + /// + /// Damage that will be applied to the victim when they are hooked or unhooked. + /// + [DataField, AutoNetworkedField] + public DamageSpecifier SpikeDamage = new() { - Status - } + DamageDict = new Dictionary + { + { "Piercing", 10 }, + }, + }; - [Serializable, NetSerializable] - public enum KitchenSpikeStatus : byte + /// + /// Damage that will be applied to the victim when they are butchered. + /// + [DataField, AutoNetworkedField] + public DamageSpecifier ButcherDamage = new() { - Empty, - Bloody - } + DamageDict = new Dictionary + { + { "Slash", 20 }, + }, + }; + + /// + /// Damage that the victim will receive over time. + /// + [DataField, AutoNetworkedField] + public DamageSpecifier TimeDamage = new() + { + DamageDict = new Dictionary + { + { "Blunt", 1 }, // Mobs are only gibbed from blunt (at least for now). + }, + }; + + /// + /// The next time when the damage will be applied to the victim. + /// + [AutoPausedField, AutoNetworkedField] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan NextDamage; + + /// + /// How often the damage should be applied to the victim. + /// + [DataField, AutoNetworkedField] + public TimeSpan DamageInterval = TimeSpan.FromSeconds(10); + + /// + /// Time that it will take to put the victim on the spike. + /// + [DataField, AutoNetworkedField] + public TimeSpan HookDelay = TimeSpan.FromSeconds(7); + + /// + /// Time that it will take to put the victim off the spike. + /// + [DataField, AutoNetworkedField] + public TimeSpan UnhookDelay = TimeSpan.FromSeconds(10); + + /// + /// Time that it will take to butcher the victim while they are alive. + /// + /// + /// This is summed up with a 's butcher delay in butcher DoAfter. + /// + [DataField, AutoNetworkedField] + public TimeSpan ButcherDelayAlive = TimeSpan.FromSeconds(8); + + /// + /// Value by which the butchering delay will be multiplied if the victim is dead. + /// + [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. } diff --git a/Content.Shared/Kitchen/Components/KitchenSpikeHookedComponent.cs b/Content.Shared/Kitchen/Components/KitchenSpikeHookedComponent.cs new file mode 100644 index 0000000000..c255db986e --- /dev/null +++ b/Content.Shared/Kitchen/Components/KitchenSpikeHookedComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Kitchen.Components; + +/// +/// Used to mark entities that are currently hooked on the spike. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedKitchenSpikeSystem))] +public sealed partial class KitchenSpikeHookedComponent : Component; diff --git a/Content.Shared/Kitchen/Components/KitchenSpikeVictimComponent.cs b/Content.Shared/Kitchen/Components/KitchenSpikeVictimComponent.cs new file mode 100644 index 0000000000..dc37592a87 --- /dev/null +++ b/Content.Shared/Kitchen/Components/KitchenSpikeVictimComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Kitchen.Components; + +/// +/// Used to mark entity that was butchered on the spike. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedKitchenSpikeSystem))] +public sealed partial class KitchenSpikeVictimComponent : Component; diff --git a/Content.Shared/Kitchen/Components/SharpComponent.cs b/Content.Shared/Kitchen/Components/SharpComponent.cs new file mode 100644 index 0000000000..3dd5e01af7 --- /dev/null +++ b/Content.Shared/Kitchen/Components/SharpComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.Nutrition.Components; +using Robust.Shared.GameStates; + +namespace Content.Shared.Kitchen.Components; + +/// +/// Applies to items that are capable of butchering entities, or +/// are otherwise sharp for some purpose. +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class SharpComponent : Component +{ + /// + /// List of the entities that are currently being butchered. + /// + // TODO just make this a tool type. Move SharpSystem to shared. + [AutoNetworkedField] + public readonly HashSet Butchering = []; + + /// + /// Affects butcher delay of the . + /// + [DataField, AutoNetworkedField] + public float ButcherDelayModifier = 1.0f; +} diff --git a/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs b/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs index 2f740f99ad..57b08569f5 100644 --- a/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs +++ b/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs @@ -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 +/// +/// Used to butcher some entities like monkeys. +/// +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(OnInit); + SubscribeLocalEvent(OnInsertAttempt); + SubscribeLocalEvent(OnEntInsertedIntoContainer); + SubscribeLocalEvent(OnEntRemovedFromContainer); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnCanDrop); + SubscribeLocalEvent(OnDragDrop); + SubscribeLocalEvent(OnSpikeHookDoAfter); + SubscribeLocalEvent(OnSpikeUnhookDoAfter); + SubscribeLocalEvent(OnSpikeButcherDoAfter); + SubscribeLocalEvent(OnSpikeExamined); + SubscribeLocalEvent>(OnGetVerbs); + SubscribeLocalEvent(OnDestruction); + + SubscribeLocalEvent(OnVictimExamined); + + // Prevent the victim from doing anything while on the spike. + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); + SubscribeLocalEvent(OnAttempt); } - private void OnCanDrop(EntityUid uid, KitchenSpikeComponent component, ref CanDropTargetEvent args) + private void OnInit(Entity ent, ref ComponentInit args) { - if (args.Handled) + ent.Comp.BodyContainer = _containerSystem.EnsureContainer(ent, ent.Comp.ContainerId); + } + + private void OnInsertAttempt(Entity ent, ref ContainerIsInsertingAttemptEvent args) + { + if (args.Cancelled || TryComp(args.EntityUid, out var butcherable) && butcherable.Type == ButcheringType.Spike) + return; + + args.Cancel(); + } + + private void OnEntInsertedIntoContainer(Entity ent, ref EntInsertedIntoContainerMessage args) + { + EnsureComp(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 ent, ref EntRemovedFromContainerMessage args) + { + RemComp(args.Entity); + _damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true); + + _appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Empty); + } + + private void OnInteractHand(Entity 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 ent, ref InteractUsingEvent args) + { + var victim = ent.Comp.BodyContainer.ContainedEntity; + + if (args.Handled || !TryComp(victim, out var butcherable) || butcherable.SpawnedEntities.Count == 0) return; args.Handled = true; - if (!HasComp(args.Dragged)) + if (!TryComp(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 ent, ref CanDropTargetEvent args) + { + if (args.Handled) + return; + + args.CanDrop = _containerSystem.CanInsert(args.Dragged, ent.Comp.BodyContainer); + args.Handled = true; + } + + private void OnDragDrop(Entity 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 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 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 ent, ref SpikeButcherDoAfterEvent args) + { + if (args.Handled || args.Cancelled || !args.Target.HasValue || !args.Used.HasValue || !TryComp(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(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 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 ent, ref GetVerbsEvent 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 ent, ref DestructionEventArgs args) + { + _containerSystem.EmptyContainer(ent.Comp.BodyContainer, destination: Transform(ent).Coordinates); + } + + private void OnVictimExamined(Entity 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(); + + 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); + } + } + + /// + /// A helper method to show predicted popups that can be targeted towards yourself or somebody else. + /// + 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); + } + + /// + /// Tries to unhook the victim. + /// + private void TryUnhook(Entity 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; diff --git a/Content.Shared/Nutrition/Components/ButcherableComponent.cs b/Content.Shared/Nutrition/Components/ButcherableComponent.cs index 4fce45422a..486026d259 100644 --- a/Content.Shared/Nutrition/Components/ButcherableComponent.cs +++ b/Content.Shared/Nutrition/Components/ButcherableComponent.cs @@ -1,34 +1,51 @@ +using Content.Shared.Kitchen; using Content.Shared.Storage; using Robust.Shared.GameStates; -namespace Content.Shared.Nutrition.Components +namespace Content.Shared.Nutrition.Components; + +/// +/// Indicates that the entity can be butchered. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ButcherableComponent : Component { /// - /// Indicates that the entity can be thrown on a kitchen spike for butchering. + /// List of the entities that this entity should spawn after being butchered. /// - [RegisterComponent, NetworkedComponent] - public sealed partial class ButcherableComponent : Component - { - [DataField("spawned", required: true)] - public List SpawnedEntities = new(); + /// + /// Note that spawns one item at a time and decreases the amount until it's zero and then removes the entry. + /// + [DataField("spawned", required: true), AutoNetworkedField] + public List SpawnedEntities = []; - [ViewVariables(VVAccess.ReadWrite), DataField("butcherDelay")] - public float ButcherDelay = 8.0f; + /// + /// Time required to butcher that entity. + /// + [DataField, AutoNetworkedField] + public float ButcherDelay = 8.0f; - [ViewVariables(VVAccess.ReadWrite), DataField("butcheringType")] - public ButcheringType Type = ButcheringType.Knife; - - /// - /// Prevents butchering same entity on two and more spikes simultaneously and multiple doAfters on the same Spike - /// - [ViewVariables] - public bool BeingButchered; - } - - public enum ButcheringType : byte - { - Knife, // e.g. goliaths - Spike, // e.g. monkeys - Gibber // e.g. humans. TODO - } + /// + /// Tool type used to butcher that entity. + /// + [DataField("butcheringType"), AutoNetworkedField] + public ButcheringType Type = ButcheringType.Knife; +} + +public enum ButcheringType : byte +{ + /// + /// E.g. goliaths. + /// + Knife, + + /// + /// E.g. monkeys. + /// + Spike, + + /// + /// E.g. humans. + /// + Gibber // TODO } diff --git a/Resources/Locale/en-US/kitchen/components/kitchen-spike-component.ftl b/Resources/Locale/en-US/kitchen/components/kitchen-spike-component.ftl index aaa1779f53..b620fdff8c 100644 --- a/Resources/Locale/en-US/kitchen/components/kitchen-spike-component.ftl +++ b/Resources/Locale/en-US/kitchen/components/kitchen-spike-component.ftl @@ -1,18 +1,37 @@ -comp-kitchen-spike-deny-collect = { CAPITALIZE(THE($this)) } already has something on it, finish collecting its meat first! -comp-kitchen-spike-deny-butcher = { CAPITALIZE(THE($victim)) } can't be butchered on { THE($this) }. -comp-kitchen-spike-deny-butcher-knife = { CAPITALIZE(THE($victim)) } can't be butchered on { THE($this) }, you need to butcher it using a knife. -comp-kitchen-spike-deny-not-dead = { CAPITALIZE(THE($victim)) } can't be butchered. { CAPITALIZE(SUBJECT($victim)) } { CONJUGATE-BE($victim) } not dead! +comp-kitchen-spike-begin-hook-self = You begin dragging yourself onto { THE($hook) }! +comp-kitchen-spike-begin-hook-self-other = { CAPITALIZE(THE($victim)) } begins dragging { REFLEXIVE($victim) } onto { THE($hook) }! -comp-kitchen-spike-begin-hook-victim = { CAPITALIZE(THE($user)) } begins dragging you onto { THE($this) }! -comp-kitchen-spike-begin-hook-self = You begin dragging yourself onto { THE($this) }! +comp-kitchen-spike-begin-hook-other-self = You begin dragging { CAPITALIZE(THE($victim)) } onto { THE($hook) }! +comp-kitchen-spike-begin-hook-other = { CAPITALIZE(THE($user)) } begins dragging { CAPITALIZE(THE($victim)) } onto { THE($hook) }!a -comp-kitchen-spike-kill = { CAPITALIZE(THE($user)) } has forced { THE($victim) } onto { THE($this) }, killing { OBJECT($victim) } instantly! +comp-kitchen-spike-hook-self = You threw yourself on { THE($hook) }! +comp-kitchen-spike-hook-self-other = { CAPITALIZE(THE($victim)) } threw { REFLEXIVE($victim) } on { THE($hook) }! -comp-kitchen-spike-suicide-other = { CAPITALIZE(THE($victim)) } threw { REFLEXIVE($victim) } on { THE($this) }! -comp-kitchen-spike-suicide-self = You throw yourself on { THE($this) }! +comp-kitchen-spike-hook-other-self = You threw { CAPITALIZE(THE($victim)) } on { THE($hook) }! +comp-kitchen-spike-hook-other = { CAPITALIZE(THE($user)) } threw { CAPITALIZE(THE($victim)) } on { THE($hook) }! -comp-kitchen-spike-knife-needed = You need a knife to do this. -comp-kitchen-spike-remove-meat = You remove some meat from { THE($victim) }. -comp-kitchen-spike-remove-meat-last = You remove the last piece of meat from { THE($victim) }! +comp-kitchen-spike-begin-unhook-self = You begin dragging yourself off { THE($hook) }! +comp-kitchen-spike-begin-unhook-self-other = { CAPITALIZE(THE($victim)) } begins dragging { REFLEXIVE($victim) } off { THE($hook) }! + +comp-kitchen-spike-begin-unhook-other-self = You begin dragging { CAPITALIZE(THE($victim)) } off { THE($hook) }! +comp-kitchen-spike-begin-unhook-other = { CAPITALIZE(THE($user)) } begins dragging { CAPITALIZE(THE($victim)) } off { THE($hook) }! + +comp-kitchen-spike-unhook-self = You got yourself off { THE($hook) }! +comp-kitchen-spike-unhook-self-other = { CAPITALIZE(THE($victim)) } got { REFLEXIVE($victim) } off { THE($hook) }! + +comp-kitchen-spike-unhook-other-self = You got { CAPITALIZE(THE($victim)) } off { THE($hook) }! +comp-kitchen-spike-unhook-other = { CAPITALIZE(THE($user)) } got { CAPITALIZE(THE($victim)) } off { THE($hook) }! + +comp-kitchen-spike-begin-butcher-self = You begin butchering { THE($victim) }! +comp-kitchen-spike-begin-butcher = { CAPITALIZE(THE($user)) } begins to butcher { THE($victim) }! + +comp-kitchen-spike-butcher-self = You butchered { THE($victim) }! +comp-kitchen-spike-butcher = { CAPITALIZE(THE($user)) } butchered { THE($victim) }! + +comp-kitchen-spike-unhook-verb = Unhook + +comp-kitchen-spike-hooked = [color=red]{ CAPITALIZE(THE($victim)) } is on this spike![/color] comp-kitchen-spike-meat-name = { $name } ({ $victim }) + +comp-kitchen-spike-victim-examine = [color=orange]{ CAPITALIZE(SUBJECT($target)) } looks quite lean.[/color] diff --git a/Resources/Prototypes/Entities/Structures/meat_spike.yml b/Resources/Prototypes/Entities/Structures/meat_spike.yml index 5825cec6ad..b8714d9d5e 100644 --- a/Resources/Prototypes/Entities/Structures/meat_spike.yml +++ b/Resources/Prototypes/Entities/Structures/meat_spike.yml @@ -50,7 +50,7 @@ enum.KitchenSpikeVisuals.Status: base: Empty: { state: spike } - Bloody: { state: spikebloody } + Bloody: { state: spikebloody } # TODO: Add sprites for different species. - type: Construction graph: MeatSpike node: MeatSpike @@ -58,3 +58,6 @@ guides: - Chef - FoodRecipes + - type: ContainerContainer + containers: + body: !type:ContainerSlot diff --git a/Resources/Prototypes/SoundCollections/kitchenspike.yml b/Resources/Prototypes/SoundCollections/kitchenspike.yml new file mode 100644 index 0000000000..e8d8dc79a6 --- /dev/null +++ b/Resources/Prototypes/SoundCollections/kitchenspike.yml @@ -0,0 +1,9 @@ +- type: soundCollection + id: Spike + files: + - /Audio/Effects/Fluids/splat.ogg + +- type: soundCollection + id: SpikeButcher + files: + - /Audio/Weapons/bladeslice.ogg