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:
@@ -1,8 +0,0 @@
|
|||||||
using Content.Shared.Kitchen;
|
|
||||||
|
|
||||||
namespace Content.Client.Kitchen;
|
|
||||||
|
|
||||||
public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Content.Server.Botany.Components;
|
using Content.Server.Botany.Components;
|
||||||
using Content.Server.Kitchen.Components;
|
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Shared.Chemistry.EntitySystems;
|
using Content.Shared.Chemistry.EntitySystems;
|
||||||
using Content.Shared.Botany;
|
using Content.Shared.Botany;
|
||||||
@@ -16,6 +15,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Shared.Administration.Logs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.Kitchen.Components;
|
||||||
|
|
||||||
namespace Content.Server.Botany.Systems;
|
namespace Content.Server.Botany.Systems;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using Content.Server.Botany.Components;
|
using Content.Server.Botany.Components;
|
||||||
using Content.Server.Kitchen.Components;
|
|
||||||
using Content.Shared.Hands.EntitySystems;
|
using Content.Shared.Hands.EntitySystems;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Kitchen.Components;
|
||||||
using Content.Shared.Random;
|
using Content.Shared.Random;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Content.Server.Atmos.EntitySystems;
|
using Content.Server.Atmos.EntitySystems;
|
||||||
using Content.Server.Botany.Components;
|
using Content.Server.Botany.Components;
|
||||||
using Content.Server.Hands.Systems;
|
using Content.Server.Hands.Systems;
|
||||||
using Content.Server.Kitchen.Components;
|
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Shared.Chemistry.EntitySystems;
|
using Content.Shared.Chemistry.EntitySystems;
|
||||||
using Content.Shared.Atmos;
|
using Content.Shared.Atmos;
|
||||||
@@ -26,6 +25,7 @@ using Robust.Shared.Timing;
|
|||||||
using Content.Shared.Administration.Logs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Containers.ItemSlots;
|
using Content.Shared.Containers.ItemSlots;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.Kitchen.Components;
|
||||||
using Content.Shared.Labels.Components;
|
using Content.Shared.Labels.Components;
|
||||||
|
|
||||||
namespace Content.Server.Botany.Systems;
|
namespace Content.Server.Botany.Systems;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Content.Server.Body.Systems;
|
using Content.Server.Body.Systems;
|
||||||
using Content.Server.Kitchen.Components;
|
|
||||||
using Content.Shared.Administration.Logs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Body.Components;
|
using Content.Shared.Body.Components;
|
||||||
using Content.Shared.Database;
|
using Content.Shared.Database;
|
||||||
@@ -8,6 +7,7 @@ using Content.Shared.DoAfter;
|
|||||||
using Content.Shared.IdentityManagement;
|
using Content.Shared.IdentityManagement;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.Kitchen;
|
using Content.Shared.Kitchen;
|
||||||
|
using Content.Shared.Kitchen.Components;
|
||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
using Content.Shared.Nutrition.Components;
|
using Content.Shared.Nutrition.Components;
|
||||||
|
|||||||
@@ -1,40 +1,143 @@
|
|||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.FixedPoint;
|
||||||
|
using Content.Shared.Nutrition.Components;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
namespace Content.Shared.Kitchen.Components;
|
namespace Content.Shared.Kitchen.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to mark entity that should act as a spike.
|
||||||
|
/// </summary>
|
||||||
[RegisterComponent, NetworkedComponent]
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
[AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||||
[Access(typeof(SharedKitchenSpikeSystem))]
|
[Access(typeof(SharedKitchenSpikeSystem))]
|
||||||
public sealed partial class KitchenSpikeComponent : Component
|
public sealed partial class KitchenSpikeComponent : Component
|
||||||
{
|
{
|
||||||
[DataField("delay")]
|
/// <summary>
|
||||||
public float SpikeDelay = 7.0f;
|
/// Default sound to play when the victim is hooked or unhooked.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpike = new("Spike");
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
/// <summary>
|
||||||
[DataField("sound")]
|
/// Default sound to play when the victim is butchered.
|
||||||
public SoundSpecifier SpikeSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
|
/// </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?)
|
/// <summary>
|
||||||
public string MeatSource1p = "?";
|
/// Container where the victim will be stored.
|
||||||
public string MeatSource0 = "?";
|
/// </summary>
|
||||||
public string Victim = "?";
|
[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)
|
/// <summary>
|
||||||
public bool InUse;
|
/// Sound to play when the victim is hooked or unhooked.
|
||||||
|
/// </summary>
|
||||||
|
[DataField, AutoNetworkedField]
|
||||||
|
public SoundSpecifier SpikeSound = new SoundCollectionSpecifier(DefaultSpike);
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
/// <summary>
|
||||||
public enum KitchenSpikeVisuals : byte
|
/// 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]
|
/// <summary>
|
||||||
public enum KitchenSpikeStatus : byte
|
/// Damage that will be applied to the victim when they are butchered.
|
||||||
|
/// </summary>
|
||||||
|
[DataField, AutoNetworkedField]
|
||||||
|
public DamageSpecifier ButcherDamage = new()
|
||||||
{
|
{
|
||||||
Empty,
|
DamageDict = new Dictionary<string, FixedPoint2>
|
||||||
Bloody
|
{
|
||||||
}
|
{ "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.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
26
Content.Shared/Kitchen/Components/SharpComponent.cs
Normal file
26
Content.Shared/Kitchen/Components/SharpComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -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.DoAfter;
|
||||||
using Content.Shared.DragDrop;
|
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.Kitchen.Components;
|
||||||
|
using Content.Shared.Mobs.Systems;
|
||||||
|
using Content.Shared.Movement.Events;
|
||||||
using Content.Shared.Nutrition.Components;
|
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.Serialization;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Content.Shared.Kitchen;
|
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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.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, 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;
|
return;
|
||||||
|
|
||||||
args.Handled = true;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Once we get silicons need to check organic
|
var victimIdentity = Identity.Entity(victim.Value, EntityManager);
|
||||||
args.CanDrop = true;
|
|
||||||
|
_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]
|
[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;
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
|
using Content.Shared.Kitchen;
|
||||||
using Content.Shared.Storage;
|
using Content.Shared.Storage;
|
||||||
using Robust.Shared.GameStates;
|
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>
|
/// <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>
|
/// </summary>
|
||||||
[RegisterComponent, NetworkedComponent]
|
/// <remarks>
|
||||||
public sealed partial class ButcherableComponent : Component
|
/// 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)]
|
[DataField("spawned", required: true), AutoNetworkedField]
|
||||||
public List<EntitySpawnEntry> SpawnedEntities = new();
|
public List<EntitySpawnEntry> SpawnedEntities = [];
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField("butcherDelay")]
|
/// <summary>
|
||||||
public float ButcherDelay = 8.0f;
|
/// Time required to butcher that entity.
|
||||||
|
/// </summary>
|
||||||
|
[DataField, AutoNetworkedField]
|
||||||
|
public float ButcherDelay = 8.0f;
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField("butcheringType")]
|
/// <summary>
|
||||||
public ButcheringType Type = ButcheringType.Knife;
|
/// Tool type used to butcher that entity.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
[DataField("butcheringType"), AutoNetworkedField]
|
||||||
/// Prevents butchering same entity on two and more spikes simultaneously and multiple doAfters on the same Spike
|
public ButcheringType Type = ButcheringType.Knife;
|
||||||
/// </summary>
|
}
|
||||||
[ViewVariables]
|
|
||||||
public bool BeingButchered;
|
public enum ButcheringType : byte
|
||||||
}
|
{
|
||||||
|
/// <summary>
|
||||||
public enum ButcheringType : byte
|
/// E.g. goliaths.
|
||||||
{
|
/// </summary>
|
||||||
Knife, // e.g. goliaths
|
Knife,
|
||||||
Spike, // e.g. monkeys
|
|
||||||
Gibber // e.g. humans. TODO
|
/// <summary>
|
||||||
}
|
/// E.g. monkeys.
|
||||||
|
/// </summary>
|
||||||
|
Spike,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// E.g. humans.
|
||||||
|
/// </summary>
|
||||||
|
Gibber // TODO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-begin-hook-self = You begin dragging yourself onto { THE($hook) }!
|
||||||
comp-kitchen-spike-deny-butcher = { CAPITALIZE(THE($victim)) } can't be butchered on { THE($this) }.
|
comp-kitchen-spike-begin-hook-self-other = { CAPITALIZE(THE($victim)) } begins dragging { REFLEXIVE($victim) } onto { THE($hook) }!
|
||||||
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-victim = { CAPITALIZE(THE($user)) } begins dragging you onto { THE($this) }!
|
comp-kitchen-spike-begin-hook-other-self = You begin dragging { CAPITALIZE(THE($victim)) } onto { THE($hook) }!
|
||||||
comp-kitchen-spike-begin-hook-self = You begin dragging yourself onto { THE($this) }!
|
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-hook-other-self = You threw { CAPITALIZE(THE($victim)) } on { THE($hook) }!
|
||||||
comp-kitchen-spike-suicide-self = You throw yourself on { THE($this) }!
|
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-begin-unhook-self = You begin dragging yourself off { THE($hook) }!
|
||||||
comp-kitchen-spike-remove-meat = You remove some meat from { THE($victim) }.
|
comp-kitchen-spike-begin-unhook-self-other = { CAPITALIZE(THE($victim)) } begins dragging { REFLEXIVE($victim) } off { THE($hook) }!
|
||||||
comp-kitchen-spike-remove-meat-last = You remove the last piece of meat from { THE($victim) }!
|
|
||||||
|
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-meat-name = { $name } ({ $victim })
|
||||||
|
|
||||||
|
comp-kitchen-spike-victim-examine = [color=orange]{ CAPITALIZE(SUBJECT($target)) } looks quite lean.[/color]
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
enum.KitchenSpikeVisuals.Status:
|
enum.KitchenSpikeVisuals.Status:
|
||||||
base:
|
base:
|
||||||
Empty: { state: spike }
|
Empty: { state: spike }
|
||||||
Bloody: { state: spikebloody }
|
Bloody: { state: spikebloody } # TODO: Add sprites for different species.
|
||||||
- type: Construction
|
- type: Construction
|
||||||
graph: MeatSpike
|
graph: MeatSpike
|
||||||
node: MeatSpike
|
node: MeatSpike
|
||||||
@@ -58,3 +58,6 @@
|
|||||||
guides:
|
guides:
|
||||||
- Chef
|
- Chef
|
||||||
- FoodRecipes
|
- FoodRecipes
|
||||||
|
- type: ContainerContainer
|
||||||
|
containers:
|
||||||
|
body: !type:ContainerSlot
|
||||||
|
|||||||
9
Resources/Prototypes/SoundCollections/kitchenspike.yml
Normal file
9
Resources/Prototypes/SoundCollections/kitchenspike.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
- type: soundCollection
|
||||||
|
id: Spike
|
||||||
|
files:
|
||||||
|
- /Audio/Effects/Fluids/splat.ogg
|
||||||
|
|
||||||
|
- type: soundCollection
|
||||||
|
id: SpikeButcher
|
||||||
|
files:
|
||||||
|
- /Audio/Weapons/bladeslice.ogg
|
||||||
Reference in New Issue
Block a user