New Feature: Kitchen spike rework (#38723)

* 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
This commit is contained in:
Winkarst-cpu
2025-08-19 20:56:36 +03:00
committed by GitHub
parent de240e1739
commit 021adbe1e1
16 changed files with 688 additions and 388 deletions

View File

@@ -1,8 +0,0 @@
using Content.Shared.Kitchen;
namespace Content.Client.Kitchen;
public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
{
}

View 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;

View 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;

View 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;

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View 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;

View File

@@ -1,40 +1,143 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.Components;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Used to mark entity that should act as a spike.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedKitchenSpikeSystem))]
public sealed partial class KitchenSpikeComponent : Component
{
[DataField("delay")]
public float SpikeDelay = 7.0f;
/// <summary>
/// Default sound to play when the victim is hooked or unhooked.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpike = new("Spike");
[ViewVariables(VVAccess.ReadWrite)]
[DataField("sound")]
public SoundSpecifier SpikeSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
/// <summary>
/// Default sound to play when the victim is butchered.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpikeButcher = new("SpikeButcher");
public List<string>? PrototypesToSpawn;
/// <summary>
/// ID of the container where the victim will be stored.
/// </summary>
[DataField, AutoNetworkedField]
public string ContainerId = "body";
// TODO: Spiking alive mobs? (Replace with uid) (deal damage to their limbs on spiking, kill on first butcher attempt?)
public string MeatSource1p = "?";
public string MeatSource0 = "?";
public string Victim = "?";
/// <summary>
/// Container where the victim will be stored.
/// </summary>
[ViewVariables]
public ContainerSlot BodyContainer = default!;
// Prevents simultaneous spiking of two bodies (could be replaced with CancellationToken, but I don't see any situation where Cancel could be called)
public bool InUse;
/// <summary>
/// Sound to play when the victim is hooked or unhooked.
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier SpikeSound = new SoundCollectionSpecifier(DefaultSpike);
[Serializable, NetSerializable]
public enum KitchenSpikeVisuals : byte
/// <summary>
/// Sound to play when the victim is butchered.
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier ButcherSound = new SoundCollectionSpecifier(DefaultSpikeButcher);
/// <summary>
/// Damage that will be applied to the victim when they are hooked or unhooked.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier SpikeDamage = new()
{
Status
}
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Piercing", 10 },
},
};
[Serializable, NetSerializable]
public enum KitchenSpikeStatus : byte
/// <summary>
/// Damage that will be applied to the victim when they are butchered.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier ButcherDamage = new()
{
Empty,
Bloody
}
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Slash", 20 },
},
};
/// <summary>
/// Damage that the victim will receive over time.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier TimeDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Blunt", 1 }, // Mobs are only gibbed from blunt (at least for now).
},
};
/// <summary>
/// The next time when the damage will be applied to the victim.
/// </summary>
[AutoPausedField, AutoNetworkedField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextDamage;
/// <summary>
/// How often the damage should be applied to the victim.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DamageInterval = TimeSpan.FromSeconds(10);
/// <summary>
/// Time that it will take to put the victim on the spike.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan HookDelay = TimeSpan.FromSeconds(7);
/// <summary>
/// Time that it will take to put the victim off the spike.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan UnhookDelay = TimeSpan.FromSeconds(10);
/// <summary>
/// Time that it will take to butcher the victim while they are alive.
/// </summary>
/// <remarks>
/// This is summed up with a <see cref="ButcherableComponent"/>'s butcher delay in butcher DoAfter.
/// </remarks>
[DataField, AutoNetworkedField]
public TimeSpan ButcherDelayAlive = TimeSpan.FromSeconds(8);
/// <summary>
/// Value by which the butchering delay will be multiplied if the victim is dead.
/// </summary>
[DataField, AutoNetworkedField]
public float ButcherModifierDead = 0.5f;
}
[Serializable, NetSerializable]
public enum KitchenSpikeVisuals : byte
{
Status,
}
[Serializable, NetSerializable]
public enum KitchenSpikeStatus : byte
{
Empty,
Bloody, // TODO: Add sprites for different species.
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,26 @@
using Content.Shared.Nutrition.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Applies to items that are capable of butchering entities, or
/// are otherwise sharp for some purpose.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class SharpComponent : Component
{
/// <summary>
/// List of the entities that are currently being butchered.
/// </summary>
// TODO just make this a tool type. Move SharpSystem to shared.
[AutoNetworkedField]
public readonly HashSet<EntityUid> Butchering = [];
/// <summary>
/// Affects butcher delay of the <see cref="ButcherableComponent"/>.
/// </summary>
[DataField, AutoNetworkedField]
public float ButcherDelayModifier = 1.0f;
}

View File

@@ -1,38 +1,456 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Systems;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Examine;
using Content.Shared.Hands;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
using Content.Shared.Kitchen.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Kitchen;
public abstract class SharedKitchenSpikeSystem : EntitySystem
/// <summary>
/// Used to butcher some entities like monkeys.
/// </summary>
public sealed class SharedKitchenSpikeSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _logger = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedBodySystem _bodySystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<KitchenSpikeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<KitchenSpikeComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<KitchenSpikeComponent, EntInsertedIntoContainerMessage>(OnEntInsertedIntoContainer);
SubscribeLocalEvent<KitchenSpikeComponent, EntRemovedFromContainerMessage>(OnEntRemovedFromContainer);
SubscribeLocalEvent<KitchenSpikeComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<KitchenSpikeComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<KitchenSpikeComponent, CanDropTargetEvent>(OnCanDrop);
SubscribeLocalEvent<KitchenSpikeComponent, DragDropTargetEvent>(OnDragDrop);
SubscribeLocalEvent<KitchenSpikeComponent, SpikeHookDoAfterEvent>(OnSpikeHookDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, SpikeUnhookDoAfterEvent>(OnSpikeUnhookDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, SpikeButcherDoAfterEvent>(OnSpikeButcherDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, ExaminedEvent>(OnSpikeExamined);
SubscribeLocalEvent<KitchenSpikeComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
SubscribeLocalEvent<KitchenSpikeComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<KitchenSpikeVictimComponent, ExaminedEvent>(OnVictimExamined);
// Prevent the victim from doing anything while on the spike.
SubscribeLocalEvent<KitchenSpikeHookedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, UpdateCanMoveEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, UseAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, ThrowAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, DropAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, AttackAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, PickupAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, IsEquippingAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, IsUnequippingAttemptEvent>(OnAttempt);
}
private void OnCanDrop(EntityUid uid, KitchenSpikeComponent component, ref CanDropTargetEvent args)
private void OnInit(Entity<KitchenSpikeComponent> ent, ref ComponentInit args)
{
if (args.Handled)
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent, ent.Comp.ContainerId);
}
private void OnInsertAttempt(Entity<KitchenSpikeComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (args.Cancelled || TryComp<ButcherableComponent>(args.EntityUid, out var butcherable) && butcherable.Type == ButcheringType.Spike)
return;
args.Cancel();
}
private void OnEntInsertedIntoContainer(Entity<KitchenSpikeComponent> ent, ref EntInsertedIntoContainerMessage args)
{
EnsureComp<KitchenSpikeHookedComponent>(args.Entity);
_damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true);
// TODO: Add sprites for different species.
_appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Bloody);
}
private void OnEntRemovedFromContainer(Entity<KitchenSpikeComponent> ent, ref EntRemovedFromContainerMessage args)
{
RemComp<KitchenSpikeHookedComponent>(args.Entity);
_damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true);
_appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Empty);
}
private void OnInteractHand(Entity<KitchenSpikeComponent> ent, ref InteractHandEvent args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (args.Handled || !victim.HasValue)
return;
_popupSystem.PopupClient(Loc.GetString("butcherable-need-knife",
("target", Identity.Entity(victim.Value, EntityManager))),
ent,
args.User,
PopupType.Medium);
args.Handled = true;
}
private void OnInteractUsing(Entity<KitchenSpikeComponent> ent, ref InteractUsingEvent args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (args.Handled || !TryComp<ButcherableComponent>(victim, out var butcherable) || butcherable.SpawnedEntities.Count == 0)
return;
args.Handled = true;
if (!HasComp<ButcherableComponent>(args.Dragged))
if (!TryComp<SharpComponent>(args.Used, out var sharp))
{
args.CanDrop = false;
_popupSystem.PopupClient(Loc.GetString("butcherable-need-knife",
("target", Identity.Entity(victim.Value, EntityManager))),
ent,
args.User,
PopupType.Medium);
return;
}
// TODO: Once we get silicons need to check organic
args.CanDrop = true;
var victimIdentity = Identity.Entity(victim.Value, EntityManager);
_popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-begin-butcher-self", ("victim", victimIdentity)),
Loc.GetString("comp-kitchen-spike-begin-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)),
ent,
args.User,
PopupType.MediumCaution);
var delay = TimeSpan.FromSeconds(sharp.ButcherDelayModifier * butcherable.ButcherDelay);
if (_mobStateSystem.IsAlive(victim.Value))
delay += ent.Comp.ButcherDelayAlive;
else
delay *= ent.Comp.ButcherModifierDead;
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
args.User,
delay,
new SpikeButcherDoAfterEvent(),
ent,
target: victim,
used: args.Used)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
});
}
private void OnCanDrop(Entity<KitchenSpikeComponent> ent, ref CanDropTargetEvent args)
{
if (args.Handled)
return;
args.CanDrop = _containerSystem.CanInsert(args.Dragged, ent.Comp.BodyContainer);
args.Handled = true;
}
private void OnDragDrop(Entity<KitchenSpikeComponent> ent, ref DragDropTargetEvent args)
{
if (args.Handled)
return;
ShowPopups("comp-kitchen-spike-begin-hook-self",
"comp-kitchen-spike-begin-hook-self-other",
"comp-kitchen-spike-begin-hook-other-self",
"comp-kitchen-spike-begin-hook-other",
args.User,
args.Dragged,
ent);
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
args.User,
ent.Comp.HookDelay,
new SpikeHookDoAfterEvent(),
ent,
target: args.Dragged)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
});
args.Handled = true;
}
private void OnSpikeHookDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeHookDoAfterEvent args)
{
if (args.Handled || args.Cancelled || !args.Target.HasValue)
return;
if (_containerSystem.Insert(args.Target.Value, ent.Comp.BodyContainer))
{
ShowPopups("comp-kitchen-spike-hook-self",
"comp-kitchen-spike-hook-self-other",
"comp-kitchen-spike-hook-other-self",
"comp-kitchen-spike-hook-other",
args.User,
args.Target.Value,
ent);
_logger.Add(LogType.Action,
LogImpact.High,
$"{ToPrettyString(args.User):user} put {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
_audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User);
}
args.Handled = true;
}
private void OnSpikeUnhookDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeUnhookDoAfterEvent args)
{
if (args.Handled || args.Cancelled || !args.Target.HasValue)
return;
if (_containerSystem.Remove(args.Target.Value, ent.Comp.BodyContainer))
{
ShowPopups("comp-kitchen-spike-unhook-self",
"comp-kitchen-spike-unhook-self-other",
"comp-kitchen-spike-unhook-other-self",
"comp-kitchen-spike-unhook-other",
args.User,
args.Target.Value,
ent);
_logger.Add(LogType.Action,
LogImpact.Medium,
$"{ToPrettyString(args.User):user} took {ToPrettyString(args.Target):target} off the {ToPrettyString(ent):spike}");
_audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User);
}
args.Handled = true;
}
private void OnSpikeButcherDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeButcherDoAfterEvent args)
{
if (args.Handled || args.Cancelled || !args.Target.HasValue || !args.Used.HasValue || !TryComp<ButcherableComponent>(args.Target, out var butcherable) )
return;
var victimIdentity = Identity.Entity(args.Target.Value, EntityManager);
_popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-butcher-self", ("victim", victimIdentity)),
Loc.GetString("comp-kitchen-spike-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)),
ent,
args.User,
PopupType.MediumCaution);
// Get a random entry to spawn.
var index = _random.Next(butcherable.SpawnedEntities.Count);
var entry = butcherable.SpawnedEntities[index];
var uid = PredictedSpawnNextToOrDrop(entry.PrototypeId, ent);
_metaDataSystem.SetEntityName(uid,
Loc.GetString("comp-kitchen-spike-meat-name",
("name", Name(uid)),
("victim", args.Target)));
// Decrease the amount since we spawned an entity from that entry.
entry.Amount--;
// Remove the entry if its new amount is zero, or update it.
if (entry.Amount <= 0)
butcherable.SpawnedEntities.RemoveAt(index);
else
butcherable.SpawnedEntities[index] = entry;
Dirty(args.Target.Value, butcherable);
// Gib the victim if there is nothing else to butcher.
if (butcherable.SpawnedEntities.Count == 0)
{
_bodySystem.GibBody(args.Target.Value, true);
_logger.Add(LogType.Gib,
LogImpact.Extreme,
$"{ToPrettyString(args.User):user} finished butchering {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
}
else
{
EnsureComp<KitchenSpikeVictimComponent>(args.Target.Value);
_damageableSystem.TryChangeDamage(args.Target, ent.Comp.ButcherDamage, true);
_logger.Add(LogType.Action,
LogImpact.Extreme,
$"{ToPrettyString(args.User):user} butchered {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
}
_audioSystem.PlayPredicted(ent.Comp.ButcherSound, ent, args.User);
_popupSystem.PopupClient(Loc.GetString("butcherable-knife-butchered-success",
("target", Identity.Entity(args.Target.Value, EntityManager)),
("knife", args.Used.Value)),
ent,
args.User,
PopupType.Medium);
args.Handled = true;
}
private void OnSpikeExamined(Entity<KitchenSpikeComponent> ent, ref ExaminedEvent args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (!victim.HasValue)
return;
// Show it at the end of the examine so it looks good.
args.PushMarkup(Loc.GetString("comp-kitchen-spike-hooked", ("victim", Identity.Entity(victim.Value, EntityManager))), -1);
args.PushMessage(_examineSystem.GetExamineText(victim.Value, args.Examiner), -2);
}
private void OnGetVerbs(Entity<KitchenSpikeComponent> ent, ref GetVerbsEvent<Verb> args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (!victim.HasValue || !_containerSystem.CanRemove(victim.Value, ent.Comp.BodyContainer))
return;
var user = args.User;
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("comp-kitchen-spike-unhook-verb"),
Act = () => TryUnhook(ent, user, victim.Value),
Impact = LogImpact.Medium,
});
}
private void OnDestruction(Entity<KitchenSpikeComponent> ent, ref DestructionEventArgs args)
{
_containerSystem.EmptyContainer(ent.Comp.BodyContainer, destination: Transform(ent).Coordinates);
}
private void OnVictimExamined(Entity<KitchenSpikeVictimComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("comp-kitchen-spike-victim-examine", ("target", Identity.Entity(ent, EntityManager))));
}
private static void OnAttempt(EntityUid uid, KitchenSpikeHookedComponent component, CancellableEntityEventArgs args)
{
args.Cancel();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = AllEntityQuery<KitchenSpikeComponent>();
while (query.MoveNext(out var uid, out var kitchenSpike))
{
if (kitchenSpike.NextDamage > _gameTiming.CurTime)
continue;
kitchenSpike.NextDamage += kitchenSpike.DamageInterval;
Dirty(uid, kitchenSpike);
_damageableSystem.TryChangeDamage(kitchenSpike.BodyContainer.ContainedEntity, kitchenSpike.TimeDamage, true);
}
}
/// <summary>
/// A helper method to show predicted popups that can be targeted towards yourself or somebody else.
/// </summary>
private void ShowPopups(string selfLocMessageSelf,
string selfLocMessageOthers,
string locMessageSelf,
string locMessageOthers,
EntityUid user,
EntityUid victim,
EntityUid hook)
{
string messageSelf, messageOthers;
var victimIdentity = Identity.Entity(victim, EntityManager);
if (user == victim)
{
messageSelf = Loc.GetString(selfLocMessageSelf, ("hook", hook));
messageOthers = Loc.GetString(selfLocMessageOthers, ("victim", victimIdentity), ("hook", hook));
}
else
{
messageSelf = Loc.GetString(locMessageSelf, ("victim", victimIdentity), ("hook", hook));
messageOthers = Loc.GetString(locMessageOthers,
("user", Identity.Entity(user, EntityManager)),
("victim", victimIdentity),
("hook", hook));
}
_popupSystem.PopupPredicted(messageSelf, messageOthers, hook, user, PopupType.MediumCaution);
}
/// <summary>
/// Tries to unhook the victim.
/// </summary>
private void TryUnhook(Entity<KitchenSpikeComponent> ent, EntityUid user, EntityUid target)
{
ShowPopups("comp-kitchen-spike-begin-unhook-self",
"comp-kitchen-spike-begin-unhook-self-other",
"comp-kitchen-spike-begin-unhook-other-self",
"comp-kitchen-spike-begin-unhook-other",
user,
target,
ent);
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
user,
ent.Comp.UnhookDelay,
new SpikeUnhookDoAfterEvent(),
ent,
target: target)
{
BreakOnDamage = user != target,
BreakOnMove = true,
});
}
}
[Serializable, NetSerializable]
public sealed partial class SpikeDoAfterEvent : SimpleDoAfterEvent
{
}
public sealed partial class SpikeHookDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable]
public sealed partial class SpikeUnhookDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable]
public sealed partial class SpikeButcherDoAfterEvent : SimpleDoAfterEvent;

View 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>
[RegisterComponent, NetworkedComponent]
public sealed partial class ButcherableComponent : Component
{
[DataField("spawned", required: true)]
public List<EntitySpawnEntry> SpawnedEntities = new();
/// <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 = [];
[ViewVariables(VVAccess.ReadWrite), DataField("butcherDelay")]
public float ButcherDelay = 8.0f;
/// <summary>
/// Time required to butcher that entity.
/// </summary>
[DataField, AutoNetworkedField]
public float ButcherDelay = 8.0f;
[ViewVariables(VVAccess.ReadWrite), DataField("butcheringType")]
public ButcheringType Type = ButcheringType.Knife;
/// <summary>
/// Prevents butchering same entity on two and more spikes simultaneously and multiple doAfters on the same Spike
/// </summary>
[ViewVariables]
public bool BeingButchered;
}
public enum ButcheringType : byte
{
Knife, // e.g. goliaths
Spike, // e.g. monkeys
Gibber // e.g. humans. TODO
}
/// <summary>
/// Tool type used to butcher that entity.
/// </summary>
[DataField("butcheringType"), AutoNetworkedField]
public ButcheringType Type = ButcheringType.Knife;
}
public enum ButcheringType : byte
{
/// <summary>
/// E.g. goliaths.
/// </summary>
Knife,
/// <summary>
/// E.g. monkeys.
/// </summary>
Spike,
/// <summary>
/// E.g. humans.
/// </summary>
Gibber // TODO
}

View 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]

View 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

View File

@@ -0,0 +1,9 @@
- type: soundCollection
id: Spike
files:
- /Audio/Effects/Fluids/splat.ogg
- type: soundCollection
id: SpikeButcher
files:
- /Audio/Weapons/bladeslice.ogg