+++ /dev/null
-using Content.Shared.Kitchen;
-
-namespace Content.Client.Kitchen;
-
-public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
-{
-
-}
using Content.Server.Botany.Components;
-using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Botany;
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
+using Content.Shared.Kitchen.Components;
namespace Content.Server.Botany.Systems;
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;
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;
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;
+++ /dev/null
-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;
-}
+++ /dev/null
-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;
- }
- }
-}
using Content.Server.Body.Systems;
-using Content.Server.Kitchen.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database;
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;
+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.
}
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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;
+}
+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;
+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
}
-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]
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
guides:
- Chef
- FoodRecipes
+ - type: ContainerContainer
+ containers:
+ body: !type:ContainerSlot
--- /dev/null
+- type: soundCollection
+ id: Spike
+ files:
+ - /Audio/Effects/Fluids/splat.ogg
+
+- type: soundCollection
+ id: SpikeButcher
+ files:
+ - /Audio/Weapons/bladeslice.ogg