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