]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
New Feature: Kitchen spike rework (#38723)
authorWinkarst-cpu <74284083+Winkarst-cpu@users.noreply.github.com>
Tue, 19 Aug 2025 17:56:36 +0000 (20:56 +0300)
committerGitHub <noreply@github.com>
Tue, 19 Aug 2025 17:56:36 +0000 (10:56 -0700)
* Start

* Wow, text

* Ultra raw

* More stuff

* Wow, DOT and gibbing!!!

* More stuff

* More

* Update

* Yes

* Almost there

* Done?

* I forgot

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Update

* Beck

* Unhardcode

16 files changed:
Content.Client/Kitchen/KitchenSpikeSystem.cs [deleted file]
Content.Server/Botany/Systems/BotanySystem.Seed.cs
Content.Server/Botany/Systems/LogSystem.cs
Content.Server/Botany/Systems/PlantHolderSystem.cs
Content.Server/Kitchen/Components/SharpComponent.cs [deleted file]
Content.Server/Kitchen/EntitySystems/KitchenSpikeSystem.cs [deleted file]
Content.Server/Kitchen/EntitySystems/SharpSystem.cs
Content.Shared/Kitchen/Components/KitchenSpikeComponent.cs
Content.Shared/Kitchen/Components/KitchenSpikeHookedComponent.cs [new file with mode: 0644]
Content.Shared/Kitchen/Components/KitchenSpikeVictimComponent.cs [new file with mode: 0644]
Content.Shared/Kitchen/Components/SharpComponent.cs [new file with mode: 0644]
Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs
Content.Shared/Nutrition/Components/ButcherableComponent.cs
Resources/Locale/en-US/kitchen/components/kitchen-spike-component.ftl
Resources/Prototypes/Entities/Structures/meat_spike.yml
Resources/Prototypes/SoundCollections/kitchenspike.yml [new file with mode: 0644]

diff --git a/Content.Client/Kitchen/KitchenSpikeSystem.cs b/Content.Client/Kitchen/KitchenSpikeSystem.cs
deleted file mode 100644 (file)
index 3627a29..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-using Content.Shared.Kitchen;
-
-namespace Content.Client.Kitchen;
-
-public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
-{
-
-}
index fd65c141aa02782be33df05a35a85275c1711df5..6b26ce7119a7fe575eb1a556c4f0207dceeafac4 100644 (file)
@@ -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;
 
index 3d415635be23e83d5f0ee720921f248fec609335..08190af7083437eb829b9b528f50cd28490f159d 100644 (file)
@@ -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;
 
index d8381b2a790cf6ec5f9d62959773e04b8f7e2e70..e38c742fa28f2c6ab7052a8c9ed1ea8a38a0bd30 100644 (file)
@@ -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 (file)
index c67c3b8..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Content.Server.Kitchen.Components;
-
-/// <summary>
-///     Applies to items that are capable of butchering entities, or
-///     are otherwise sharp for some purpose.
-/// </summary>
-[RegisterComponent]
-public sealed partial class SharpComponent : Component
-{
-    // TODO just make this a tool type.
-    public HashSet<EntityUid> 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 (file)
index 4ed05a3..0000000
+++ /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<KitchenSpikeComponent, InteractUsingEvent>(OnInteractUsing);
-            SubscribeLocalEvent<KitchenSpikeComponent, InteractHandEvent>(OnInteractHand);
-            SubscribeLocalEvent<KitchenSpikeComponent, DragDropTargetEvent>(OnDragDrop);
-
-            //DoAfter
-            SubscribeLocalEvent<KitchenSpikeComponent, SpikeDoAfterEvent>(OnDoAfter);
-
-            SubscribeLocalEvent<KitchenSpikeComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
-
-            SubscribeLocalEvent<ButcherableComponent, CanDropDraggedEvent>(OnButcherableCanDrop);
-        }
-
-        private void OnButcherableCanDrop(Entity<ButcherableComponent> entity, ref CanDropDraggedEvent args)
-        {
-            args.Handled = true;
-            args.CanDrop |= entity.Comp.Type != ButcheringType.Knife;
-        }
-
-        /// <summary>
-        /// TODO: Update this so it actually meatspikes the user instead of applying lethal damage to them.
-        /// </summary>
-        private void OnSuicideByEnvironment(Entity<KitchenSpikeComponent> entity, ref SuicideByEnvironmentEvent args)
-        {
-            if (args.Handled)
-                return;
-
-            if (!TryComp<DamageableComponent>(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<KitchenSpikeComponent> entity, ref SpikeDoAfterEvent args)
-        {
-            if (args.Args.Target == null)
-                return;
-
-            if (TryComp<ButcherableComponent>(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<KitchenSpikeComponent> 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<KitchenSpikeComponent> 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<KitchenSpikeComponent> 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<HumanoidAppearanceComponent>(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;
-        }
-    }
-}
index 0275e4d1a78a4400ad829b516a21777ac3621a3f..ab6e1db49404672cdec794aad575e9b8974ed22c 100644 (file)
@@ -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;
index 3057a75a4ccb121534374a60c33c8a91e06e0954..b4fdc5ed3cc92ff726993364e846339a246a4140 100644 (file)
+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()
+    {
+        DamageDict = new Dictionary<string, FixedPoint2>
+        {
+            { "Piercing", 10 },
+        },
+    };
+
+    /// <summary>
+    /// Damage that will be applied to the victim when they are butchered.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public DamageSpecifier ButcherDamage = new()
     {
-        Status
-    }
+        DamageDict = new Dictionary<string, FixedPoint2>
+        {
+            { "Slash", 20 },
+        },
+    };
 
-    [Serializable, NetSerializable]
-    public enum KitchenSpikeStatus : byte
+    /// <summary>
+    /// Damage that the victim will receive over time.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public DamageSpecifier TimeDamage = new()
     {
-        Empty,
-        Bloody
-    }
+        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.
 }
diff --git a/Content.Shared/Kitchen/Components/KitchenSpikeHookedComponent.cs b/Content.Shared/Kitchen/Components/KitchenSpikeHookedComponent.cs
new file mode 100644 (file)
index 0000000..c255db9
--- /dev/null
@@ -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;
diff --git a/Content.Shared/Kitchen/Components/KitchenSpikeVictimComponent.cs b/Content.Shared/Kitchen/Components/KitchenSpikeVictimComponent.cs
new file mode 100644 (file)
index 0000000..dc37592
--- /dev/null
@@ -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;
diff --git a/Content.Shared/Kitchen/Components/SharpComponent.cs b/Content.Shared/Kitchen/Components/SharpComponent.cs
new file mode 100644 (file)
index 0000000..3dd5e01
--- /dev/null
@@ -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;
+}
index 2f740f99ad8e8b9a63695ce09d9e58c4e246adf9..57b08569f561366155766bc08d108c54c1379f16 100644 (file)
+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 OnInit(Entity<KitchenSpikeComponent> ent, ref ComponentInit args)
+    {
+        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 (!TryComp<SharpComponent>(args.Used, out var sharp))
+        {
+            _popupSystem.PopupClient(Loc.GetString("butcherable-need-knife",
+                    ("target", Identity.Entity(victim.Value, EntityManager))),
+                    ent,
+                    args.User,
+                    PopupType.Medium);
+
+            return;
+        }
+
+        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(EntityUid uid, KitchenSpikeComponent component, ref CanDropTargetEvent args)
+    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);
 
-        if (!HasComp<ButcherableComponent>(args.Dragged))
+        // 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)
         {
-            args.CanDrop = false;
+            _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);
         }
+    }
 
-        // TODO: Once we get silicons need to check organic
-        args.CanDrop = 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;
index 4fce45422ad137c72d98cbb03ef9b11ec646c567..486026d25903b48b71b1e28e3267ece791b6e36e 100644 (file)
@@ -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;
+
+/// <summary>
+/// Indicates that the entity can be butchered.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ButcherableComponent : Component
 {
     /// <summary>
-    /// 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.
+    /// </summary>
+    /// <remarks>
+    /// Note that <see cref="SharedKitchenSpikeSystem"/> spawns one item at a time and decreases the amount until it's zero and then removes the entry.
+    /// </remarks>
+    [DataField("spawned", required: true), AutoNetworkedField]
+    public List<EntitySpawnEntry> SpawnedEntities = [];
+
+    /// <summary>
+    /// Time required to butcher that entity.
     /// </summary>
-    [RegisterComponent, NetworkedComponent]
-    public sealed partial class ButcherableComponent : Component
-    {
-        [DataField("spawned", required: true)]
-        public List<EntitySpawnEntry> SpawnedEntities = new();
+    [DataField, AutoNetworkedField]
+    public float ButcherDelay = 8.0f;
 
-        [ViewVariables(VVAccess.ReadWrite), DataField("butcherDelay")]
-        public float ButcherDelay = 8.0f;
+    /// <summary>
+    /// Tool type used to butcher that entity.
+    /// </summary>
+    [DataField("butcheringType"), AutoNetworkedField]
+    public ButcheringType Type = ButcheringType.Knife;
+}
 
-        [ViewVariables(VVAccess.ReadWrite), DataField("butcheringType")]
-        public ButcheringType Type = ButcheringType.Knife;
+public enum ButcheringType : byte
+{
+    /// <summary>
+    /// E.g. goliaths.
+    /// </summary>
+    Knife,
 
-        /// <summary>
-        /// Prevents butchering same entity on two and more spikes simultaneously and multiple doAfters on the same Spike
-        /// </summary>
-        [ViewVariables]
-        public bool BeingButchered;
-    }
+    /// <summary>
+    /// E.g. monkeys.
+    /// </summary>
+    Spike,
 
-    public enum ButcheringType : byte
-    {
-        Knife, // e.g. goliaths
-        Spike, // e.g. monkeys
-        Gibber // e.g. humans. TODO
-    }
+    /// <summary>
+    /// E.g. humans.
+    /// </summary>
+    Gibber // TODO
 }
index aaa1779f5312d8057eda594db959a0ce915218d7..b620fdff8c9fbd11697b68575e77b2f1947f4941 100644 (file)
@@ -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]
index 5825cec6ad262cc455398df330302ee6074b7396..b8714d9d5ea3cb8c83e79a2c9d87e5d568290a3d 100644 (file)
@@ -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 (file)
index 0000000..e8d8dc7
--- /dev/null
@@ -0,0 +1,9 @@
+- type: soundCollection
+  id: Spike
+  files:
+  - /Audio/Effects/Fluids/splat.ogg
+
+- type: soundCollection
+  id: SpikeButcher
+  files:
+  - /Audio/Weapons/bladeslice.ogg