Change all of body system to use entities and components (#2074)
* Early commit * Early commit 2 * merging master broke my git * does anyone even read these * life is fleeting * it just works * this time passing integration tests * Remove hashset yaml serialization for now * You got a license for those nullables? * No examine, no context menu, part and mechanism parenting and visibility * Fix wrong brain sprite state * Removing layers was a mistake * just tear body system a new one and see if it still breathes * Remove redundant code * Add that comment back * Separate damage and body, component states, stomach rework * Add containers for body parts * Bring layers back pls * Fix parts magically changing color * Reimplement sprite layer visibility * Fix tests * Add leg test * Active legs is gone Crab rave * Merge fixes, rename DamageState to CurrentState * Remove IShowContextMenu and ICanExamine
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class BrainBehaviorComponent : MechanismBehaviorComponent
|
||||
{
|
||||
public override string Name => "Brain";
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
// TODO BODY
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public interface IMechanismBehavior : IHasBody
|
||||
{
|
||||
IBodyPart? Part { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Upward reference to the parent <see cref="IMechanism"/> that this
|
||||
/// behavior is attached to.
|
||||
/// </summary>
|
||||
IMechanism? Mechanism { get; }
|
||||
|
||||
void Update(float frameTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public abstract class MechanismBehaviorComponent : Component, IMechanismBehavior
|
||||
{
|
||||
public IBody? Body => Part?.Body;
|
||||
|
||||
public IBodyPart? Part => Mechanism?.Part;
|
||||
|
||||
public IMechanism? Mechanism => Owner.GetComponentOrNull<IMechanism>();
|
||||
|
||||
public abstract void Update(float frameTime);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the containing <see cref="IBodyPart"/> is attached to a
|
||||
/// <see cref="IBody"/>.
|
||||
/// For instance, attaching a head to a body will call this on the brain inside.
|
||||
/// </summary>
|
||||
public void AddedToBody()
|
||||
{
|
||||
OnAddedToBody();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the parent <see cref="Mechanism"/> is
|
||||
/// added into a <see cref="IBodyPart"/>.
|
||||
/// For instance, putting a brain into an empty head.
|
||||
/// </summary>
|
||||
public void AddedToPart()
|
||||
{
|
||||
OnAddedToPart();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the containing <see cref="IBodyPart"/> is removed from a
|
||||
/// <see cref="IBody"/>.
|
||||
/// For instance, cutting off ones head will call this on the brain inside.
|
||||
/// </summary>
|
||||
public void RemovedFromBody(IBody old)
|
||||
{
|
||||
OnRemovedFromBody(old);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the parent <see cref="Mechanism"/> is
|
||||
/// removed from a <see cref="IBodyPart"/>.
|
||||
/// For instance, taking a brain out of ones head.
|
||||
/// </summary>
|
||||
public void RemovedFromPart(IBodyPart old)
|
||||
{
|
||||
OnRemovedFromPart(old);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the containing <see cref="IBodyPart"/> is attached to a
|
||||
/// <see cref="IBody"/>.
|
||||
/// For instance, attaching a head to a body will call this on the brain inside.
|
||||
/// </summary>
|
||||
protected virtual void OnAddedToBody() { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the parent <see cref="Mechanism"/> is
|
||||
/// added into a <see cref="IBodyPart"/>.
|
||||
/// For instance, putting a brain into an empty head.
|
||||
/// </summary>
|
||||
protected virtual void OnAddedToPart() { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the containing <see cref="IBodyPart"/> is removed from a
|
||||
/// <see cref="IBody"/>.
|
||||
/// For instance, cutting off ones head will call this on the brain inside.
|
||||
/// </summary>
|
||||
protected virtual void OnRemovedFromBody(IBody old) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the parent <see cref="Mechanism"/> is
|
||||
/// removed from a <see cref="IBodyPart"/>.
|
||||
/// For instance, taking a brain out of ones head.
|
||||
/// </summary>
|
||||
protected virtual void OnRemovedFromPart(IBodyPart old) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#nullable enable
|
||||
namespace Content.Shared.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public abstract class SharedHeartBehaviorComponent : MechanismBehaviorComponent
|
||||
{
|
||||
public override string Name => "Heart";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#nullable enable
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public abstract class SharedLungBehaviorComponent : MechanismBehaviorComponent
|
||||
{
|
||||
public override string Name => "Lung";
|
||||
|
||||
[ViewVariables] public abstract float Temperature { get; }
|
||||
|
||||
[ViewVariables] public abstract float Volume { get; }
|
||||
|
||||
[ViewVariables] public LungStatus Status { get; set; }
|
||||
|
||||
[ViewVariables] public float CycleDelay { get; set; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(this, l => l.CycleDelay, "cycleDelay", 2);
|
||||
}
|
||||
|
||||
public abstract void Inhale(float frameTime);
|
||||
|
||||
public abstract void Exhale(float frameTime);
|
||||
|
||||
public abstract void Gasp();
|
||||
}
|
||||
|
||||
public enum LungStatus
|
||||
{
|
||||
None = 0,
|
||||
Inhaling,
|
||||
Exhaling
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body.Networks;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Where reagents go when ingested. Tracks ingested reagents over time, and
|
||||
/// eventually transfers them to <see cref="SharedBloodstreamComponent"/> once digested.
|
||||
/// </summary>
|
||||
public abstract class SharedStomachBehaviorComponent : MechanismBehaviorComponent
|
||||
{
|
||||
public override string Name => "Stomach";
|
||||
|
||||
private float _accumulatedFrameTime;
|
||||
|
||||
/// <summary>
|
||||
/// Updates digestion status of ingested reagents.
|
||||
/// Once reagents surpass _digestionDelay they are moved to the
|
||||
/// bloodstream, where they are then metabolized.
|
||||
/// </summary>
|
||||
/// <param name="frameTime">
|
||||
/// The time since the last update in seconds.
|
||||
/// </param>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulatedFrameTime += frameTime;
|
||||
|
||||
// Update at most once per second
|
||||
if (_accumulatedFrameTime < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulatedFrameTime -= 1;
|
||||
|
||||
if (!Body.Owner.TryGetComponent(out SharedSolutionContainerComponent? solution) ||
|
||||
!Body.Owner.TryGetComponent(out SharedBloodstreamComponent? bloodstream))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add reagents ready for transfer to bloodstream to transferSolution
|
||||
var transferSolution = new Solution();
|
||||
|
||||
// Use ToList here to remove entries while iterating
|
||||
foreach (var delta in _reagentDeltas.ToList())
|
||||
{
|
||||
//Increment lifetime of reagents
|
||||
delta.Increment(frameTime);
|
||||
if (delta.Lifetime > _digestionDelay)
|
||||
{
|
||||
solution.TryRemoveReagent(delta.ReagentId, delta.Quantity);
|
||||
transferSolution.AddReagent(delta.ReagentId, delta.Quantity);
|
||||
_reagentDeltas.Remove(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer digested reagents to bloodstream
|
||||
bloodstream.TryTransferSolution(transferSolution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Max volume of internal solution storage
|
||||
/// </summary>
|
||||
public ReagentUnit MaxVolume
|
||||
{
|
||||
get => Owner.TryGetComponent(out SharedSolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
|
||||
set
|
||||
{
|
||||
if (Owner.TryGetComponent(out SharedSolutionContainerComponent? solution))
|
||||
{
|
||||
solution.MaxVolume = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initial internal solution storage volume
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
protected ReagentUnit InitialMaxVolume { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time in seconds between reagents being ingested and them being
|
||||
/// transferred to <see cref="SharedBloodstreamComponent"/>
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private float _digestionDelay;
|
||||
|
||||
/// <summary>
|
||||
/// Used to track how long each reagent has been in the stomach
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private readonly List<ReagentDelta> _reagentDeltas = new List<ReagentDelta>();
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(this, s => s.InitialMaxVolume, "maxVolume", ReagentUnit.New(100));
|
||||
serializer.DataField(ref _digestionDelay, "digestionDelay", 20);
|
||||
}
|
||||
|
||||
public bool CanTransferSolution(Solution solution)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SharedSolutionContainerComponent? solutionComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: For now no partial transfers. Potentially change by design
|
||||
if (!solutionComponent.CanAddSolution(solution))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTransferSolution(Solution solution)
|
||||
{
|
||||
if (!CanTransferSolution(solution))
|
||||
return false;
|
||||
|
||||
if (!Owner.TryGetComponent(out SharedSolutionContainerComponent? solutionComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add solution to _stomachContents
|
||||
solutionComponent.TryAddSolution(solution, false, true);
|
||||
// Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
|
||||
foreach (var reagent in solution.Contents)
|
||||
{
|
||||
_reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to track quantity changes when ingesting & digesting reagents
|
||||
/// </summary>
|
||||
protected class ReagentDelta
|
||||
{
|
||||
public readonly string ReagentId;
|
||||
public readonly ReagentUnit Quantity;
|
||||
public float Lifetime { get; private set; }
|
||||
|
||||
public ReagentDelta(string reagentId, ReagentUnit quantity)
|
||||
{
|
||||
ReagentId = reagentId;
|
||||
Quantity = quantity;
|
||||
Lifetime = 0.0f;
|
||||
}
|
||||
|
||||
public void Increment(float delta) => Lifetime += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Content.Shared/GameObjects/Components/Body/BodyExtensions.cs
Normal file
29
Content.Shared/GameObjects/Components/Body/BodyExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body
|
||||
{
|
||||
public static class BodyExtensions
|
||||
{
|
||||
public static T? GetBody<T>(this IEntity entity) where T : class, IBody
|
||||
{
|
||||
return entity.GetComponentOrNull<T>();
|
||||
}
|
||||
|
||||
public static bool TryGetBody<T>(this IEntity entity, [NotNullWhen(true)] out T? body) where T : class, IBody
|
||||
{
|
||||
return (body = entity.GetBody<T>()) != null;
|
||||
}
|
||||
|
||||
public static IBody? GetBody(this IEntity entity)
|
||||
{
|
||||
return entity.GetComponentOrNull<IBody>();
|
||||
}
|
||||
|
||||
public static bool TryGetBody(this IEntity entity, [NotNullWhen(true)] out IBody? body)
|
||||
{
|
||||
return (body = entity.GetBody()) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
208
Content.Shared/GameObjects/Components/Body/IBody.cs
Normal file
208
Content.Shared/GameObjects/Components/Body/IBody.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Body.Part.Property;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body
|
||||
{
|
||||
/// <summary>
|
||||
/// Component representing a collection of <see cref="IBodyPart"/>s
|
||||
/// attached to each other.
|
||||
/// </summary>
|
||||
public interface IBody : IComponent, IBodyPartContainer
|
||||
{
|
||||
public string? TemplateName { get; }
|
||||
|
||||
public string? PresetName { get; }
|
||||
|
||||
// TODO BODY tf is this
|
||||
/// <summary>
|
||||
/// Maps all parts on this template to its BodyPartType.
|
||||
/// For instance, "right arm" is mapped to "BodyPartType.arm" on the humanoid
|
||||
/// template.
|
||||
/// </summary>
|
||||
public Dictionary<string, BodyPartType> Slots { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps slots to the part filling each one.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, IBodyPart> Parts { get; }
|
||||
|
||||
// TODO BODY what am i doing
|
||||
/// <summary>
|
||||
/// Maps limb name to the list of their connections to other limbs.
|
||||
/// For instance, on the humanoid template "torso" is mapped to a list
|
||||
/// containing "right arm", "left arm", "left leg", and "right leg".
|
||||
/// This is mapped both ways during runtime, but in the prototype only one
|
||||
/// way has to be defined, i.e., "torso" to "left arm" will automatically
|
||||
/// map "left arm" to "torso".
|
||||
/// </summary>
|
||||
public Dictionary<string, List<string>> Connections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maps a template slot to the ID of the <see cref="IBodyPart"/>
|
||||
/// that should fill it. E.g. "right arm" : "BodyPart.arm.basic_human".
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> PartIds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given <see cref="IBodyPart"/> into the given slot.
|
||||
/// </summary>
|
||||
/// <returns>True if successful, false otherwise.</returns>
|
||||
bool TryAddPart(string slot, IBodyPart part, bool force = false);
|
||||
|
||||
bool HasPart(string slot);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the given <see cref="IBodyPart"/> reference, potentially
|
||||
/// dropping other <see cref="IBodyPart">BodyParts</see> if they
|
||||
/// were hanging off of it.
|
||||
/// </summary>
|
||||
void RemovePart(IBodyPart part, bool drop);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the body part in slot <see cref="slot"/> from this body,
|
||||
/// if one exists.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot to remove it from.</param>
|
||||
/// <param name="drop">
|
||||
/// Whether or not to drop the removed <see cref="IBodyPart"/>.
|
||||
/// </param>
|
||||
/// <returns>True if the part was removed, false otherwise.</returns>
|
||||
bool RemovePart(string slot, bool drop);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the body part from this body, if one exists.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to remove from this body.</param>
|
||||
/// <param name="slotName">The slot that the part was in, if any.</param>
|
||||
/// <returns>True if <see cref="part"/> was removed, false otherwise.</returns>
|
||||
bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slotName);
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the given <see cref="IBodyPart"/> reference, potentially
|
||||
/// dropping other <see cref="IBodyPart">BodyParts</see> if they
|
||||
/// were hanging off of it.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to drop.</param>
|
||||
/// <param name="dropped">
|
||||
/// All of the parts that were dropped, including <see cref="part"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// True if the part was dropped, false otherwise.
|
||||
/// </returns>
|
||||
bool TryDropPart(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart>? dropped);
|
||||
|
||||
/// <summary>
|
||||
/// Recursively searches for if <see cref="part"/> is connected to
|
||||
/// the center.
|
||||
/// </summary>
|
||||
/// <param name="part">The body part to find the center for.</param>
|
||||
/// <returns>True if it is connected to the center, false otherwise.</returns>
|
||||
bool ConnectedToCenter(IBodyPart part);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the central <see cref="IBodyPart"/>, if any, of this body based on
|
||||
/// the <see cref="BodyTemplate"/>. For humans, this is the torso.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="BodyPart"/> if one exists, null otherwise.</returns>
|
||||
IBodyPart? CenterPart();
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given part slot name exists within the current
|
||||
/// <see cref="BodyTemplate"/>.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot to check for.</param>
|
||||
/// <returns>True if the slot exists in this body, false otherwise.</returns>
|
||||
bool HasSlot(string slot);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the <see cref="IBodyPart"/> in the given <see cref="slot"/> if
|
||||
/// one exists.
|
||||
/// </summary>
|
||||
/// <param name="slot">The part slot to search in.</param>
|
||||
/// <param name="result">The body part in that slot, if any.</param>
|
||||
/// <returns>True if found, false otherwise.</returns>
|
||||
bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the slotName that the given <see cref="IBodyPart"/> resides in.
|
||||
/// </summary>
|
||||
/// <param name="part">
|
||||
/// The <see cref="IBodyPart"/> to find the slot for.
|
||||
/// </param>
|
||||
/// <param name="slot">The slot found, if any.</param>
|
||||
/// <returns>True if a slot was found, false otherwise</returns>
|
||||
bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the <see cref="BodyPartType"/> in the given
|
||||
/// <see cref="slot"/> if one exists.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot to search in.</param>
|
||||
/// <param name="result">
|
||||
/// The <see cref="BodyPartType"/> of that slot, if any.
|
||||
/// </param>
|
||||
/// <returns>True if found, false otherwise.</returns>
|
||||
bool TryGetSlotType(string slot, out BodyPartType result);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the names of all slots connected to the given
|
||||
/// <see cref="slot"/> for the template.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot to search in.</param>
|
||||
/// <param name="connections">The connections found, if any.</param>
|
||||
/// <returns>True if the connections are found, false otherwise.</returns>
|
||||
bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List<string>? connections);
|
||||
|
||||
/// <summary>
|
||||
/// Grabs all occupied slots connected to the given slot,
|
||||
/// regardless of whether the given <see cref="slot"/> is occupied.
|
||||
/// </summary>
|
||||
/// <param name="slot">The slot name to find connections from.</param>
|
||||
/// <param name="connections">The connected body parts, if any.</param>
|
||||
/// <returns>
|
||||
/// True if successful, false if the slot couldn't be found on this body.
|
||||
/// </returns>
|
||||
bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List<IBodyPart>? connections);
|
||||
|
||||
/// <summary>
|
||||
/// Grabs all parts connected to the given <see cref="part"/>, regardless
|
||||
/// of whether the given <see cref="part"/> is occupied.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to find connections from.</param>
|
||||
/// <param name="connections">The connected body parts, if any.</param>
|
||||
/// <returns>
|
||||
/// True if successful, false if the part couldn't be found on this body.
|
||||
/// </returns>
|
||||
bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart>? connections);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all <see cref="IBodyPart"/>s of the given type in this body.
|
||||
/// </summary>
|
||||
/// <returns>A list of parts of that type.</returns>
|
||||
List<IBodyPart> GetPartsOfType(BodyPartType type);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all <see cref="IBodyPart"/>s with the given property in this body.
|
||||
/// </summary>
|
||||
/// <type name="type">The property type to look for.</type>
|
||||
/// <returns>A list of parts with that property.</returns>
|
||||
List<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all <see cref="IBodyPart"/>s with the given property in this body.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The property type to look for.</typeparam>
|
||||
/// <returns>A list of parts with that property.</returns>
|
||||
List<(IBodyPart part, T property)> GetPartsWithProperty<T>() where T : class, IBodyPartProperty;
|
||||
|
||||
// TODO BODY Make a slot object that makes sense to the human mind, and make it serializable. Imagine the possibilities!
|
||||
KeyValuePair<string, BodyPartType> SlotAt(int index);
|
||||
|
||||
KeyValuePair<string, IBodyPart> PartAt(int index);
|
||||
}
|
||||
}
|
||||
13
Content.Shared/GameObjects/Components/Body/IHasBody.cs
Normal file
13
Content.Shared/GameObjects/Components/Body/IHasBody.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body
|
||||
{
|
||||
public interface IHasBody : IComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The body that this component is currently a part of, if any.
|
||||
/// </summary>
|
||||
IBody? Body { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body
|
||||
{
|
||||
public interface ISharedBodyManagerComponent : IDamageableComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Mechanism
|
||||
{
|
||||
public interface IMechanism : IHasBody
|
||||
{
|
||||
IBodyPart? Part { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Professional description of the <see cref="IMechanism"/>.
|
||||
/// </summary>
|
||||
string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The message to display upon examining a mob with this
|
||||
/// <see cref="IMechanism"/> added.
|
||||
/// If the string is empty (""), no message will be displayed.
|
||||
/// </summary>
|
||||
string ExamineMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Max HP of this <see cref="IMechanism"/>.
|
||||
/// </summary>
|
||||
int MaxDurability { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current HP of this <see cref="IMechanism"/>.
|
||||
/// </summary>
|
||||
int CurrentDurability { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// At what HP this <see cref="IMechanism"/> is completely destroyed.
|
||||
/// </summary>
|
||||
int DestroyThreshold { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Armor of this <see cref="IMechanism"/> against attacks.
|
||||
/// </summary>
|
||||
int Resistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines a handful of things - mostly whether this
|
||||
/// <see cref="IMechanism"/> can fit into a <see cref="IBodyPart"/>.
|
||||
/// </summary>
|
||||
// TODO BODY OnSizeChanged
|
||||
int Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What kind of <see cref="IBodyPart"/> this
|
||||
/// <see cref="IMechanism"/> can be easily installed into.
|
||||
/// </summary>
|
||||
BodyPartCompatibility Compatibility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the part housing this mechanism is added to a body.
|
||||
/// DO NOT CALL THIS DIRECTLY FROM OUTSIDE BODY PART CODE!
|
||||
/// </summary>
|
||||
/// <param name="old">The previous body, if any.</param>
|
||||
/// <param name="current">The new body.</param>
|
||||
void OnBodyAdd(IBody? old, IBody current);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the part housing this mechanism is removed from
|
||||
/// a body.
|
||||
/// DO NOT CALL THIS DIRECTLY FROM OUTSIDE BODY PART CODE!
|
||||
/// </summary>
|
||||
/// <param name="old">The old body.</param>
|
||||
void OnBodyRemove(IBody old);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Body.Behavior;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Mechanism
|
||||
{
|
||||
public static class MechanismExtensions
|
||||
{
|
||||
public static bool HasMechanismBehavior<T>(this IEntity entity) where T : IMechanismBehavior
|
||||
{
|
||||
// TODO BODY optimize
|
||||
return entity.TryGetBody(out var body) &&
|
||||
body.Parts.Values.Any(p => p.Mechanisms.Any(m => m.Owner.HasComponent<T>()));
|
||||
}
|
||||
|
||||
public static IEnumerable<T> GetMechanismBehaviors<T>(this IEntity entity) where T : class, IMechanismBehavior
|
||||
{
|
||||
if (!entity.TryGetBody(out var body))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var part in body.Parts.Values)
|
||||
foreach (var mechanism in part.Mechanisms)
|
||||
{
|
||||
if (mechanism.Owner.TryGetComponent(out T? behavior))
|
||||
{
|
||||
yield return behavior;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetMechanismBehaviors<T>(this IEntity entity, [NotNullWhen(true)] out List<T>? behaviors)
|
||||
where T : class, IMechanismBehavior
|
||||
{
|
||||
behaviors = entity.GetMechanismBehaviors<T>().ToList();
|
||||
|
||||
if (behaviors.Count == 0)
|
||||
{
|
||||
behaviors = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Mechanism
|
||||
{
|
||||
public abstract class SharedMechanismComponent : Component, IMechanism
|
||||
{
|
||||
public override string Name => "Mechanism";
|
||||
|
||||
private IBodyPart? _part;
|
||||
|
||||
protected readonly Dictionary<int, object> OptionsCache = new Dictionary<int, object>();
|
||||
|
||||
protected IBody? BodyCache;
|
||||
|
||||
protected int IdHash;
|
||||
|
||||
protected IEntity? PerformerCache;
|
||||
|
||||
public IBody? Body => Part?.Body;
|
||||
|
||||
public IBodyPart? Part
|
||||
{
|
||||
get => _part;
|
||||
set
|
||||
{
|
||||
if (_part == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var old = _part;
|
||||
_part = value;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
OnPartAdd(old, value);
|
||||
}
|
||||
else if (old != null)
|
||||
{
|
||||
OnPartRemove(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public string ExamineMessage { get; set; } = string.Empty;
|
||||
|
||||
public int MaxDurability { get; set; }
|
||||
|
||||
public int CurrentDurability { get; set; }
|
||||
|
||||
public int DestroyThreshold { get; set; }
|
||||
|
||||
// TODO BODY
|
||||
public int Resistance { get; set; }
|
||||
|
||||
// TODO BODY OnSizeChanged
|
||||
public int Size { get; set; }
|
||||
|
||||
public BodyPartCompatibility Compatibility { get; set; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(this, m => m.Description, "description", string.Empty);
|
||||
|
||||
serializer.DataField(this, m => m.ExamineMessage, "examineMessage", string.Empty);
|
||||
|
||||
serializer.DataField(this, m => m.MaxDurability, "maxDurability", 10);
|
||||
|
||||
serializer.DataField(this, m => m.CurrentDurability, "currentDurability", MaxDurability);
|
||||
|
||||
serializer.DataField(this, m => m.DestroyThreshold, "destroyThreshold", -MaxDurability);
|
||||
|
||||
serializer.DataField(this, m => m.Resistance, "resistance", 0);
|
||||
|
||||
serializer.DataField(this, m => m.Size, "size", 1);
|
||||
|
||||
serializer.DataField(this, m => m.Compatibility, "compatibility", BodyPartCompatibility.Universal);
|
||||
}
|
||||
|
||||
public virtual void OnBodyAdd(IBody? old, IBody current) { }
|
||||
|
||||
public virtual void OnBodyRemove(IBody old) { }
|
||||
|
||||
protected virtual void OnPartAdd(IBodyPart? old, IBodyPart current)
|
||||
{
|
||||
Owner.Transform.AttachParent(current.Owner);
|
||||
}
|
||||
|
||||
protected virtual void OnPartRemove(IBodyPart old)
|
||||
{
|
||||
Owner.Transform.AttachToGridOrMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Content.Shared.Chemistry;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Networks
|
||||
{
|
||||
public abstract class SharedBloodstreamComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempt to transfer provided solution to internal solution.
|
||||
/// Only supports complete transfers
|
||||
/// </summary>
|
||||
/// <param name="solution">Solution to be transferred</param>
|
||||
/// <returns>Whether or not transfer was a success</returns>
|
||||
public abstract bool TryTransferSolution(Solution solution);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to determine whether a BodyPart can connect to another BodyPart.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum BodyPartCompatibility
|
||||
{
|
||||
Universal = 0,
|
||||
Biological,
|
||||
Mechanical
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.GameObjects.Components.Body.Part.Property;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
public static class BodyPartExtensions
|
||||
{
|
||||
public static bool HasProperty(this IBodyPart part, Type type)
|
||||
{
|
||||
return part.Owner.HasComponent(type);
|
||||
}
|
||||
|
||||
public static bool HasProperty<T>(this IBodyPart part) where T : class, IBodyPartProperty
|
||||
{
|
||||
return part.HasProperty(typeof(T));
|
||||
}
|
||||
|
||||
public static bool TryGetProperty(this IBodyPart part, Type type,
|
||||
[NotNullWhen(true)] out IBodyPartProperty? property)
|
||||
{
|
||||
if (!part.Owner.TryGetComponent(type, out var component))
|
||||
{
|
||||
property = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return (property = component as IBodyPartProperty) != null;
|
||||
}
|
||||
|
||||
public static bool TryGetProperty<T>(this IBodyPart part, [NotNullWhen(true)] out T? property) where T : class, IBodyPartProperty
|
||||
{
|
||||
return part.Owner.TryGetComponent(out property);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum BodyPartSymmetry
|
||||
{
|
||||
None = 0,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
/// <summary>
|
||||
/// Each BodyPart has a BodyPartType used to determine a variety of things.
|
||||
/// For instance, what slots it can fit into.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum BodyPartType
|
||||
{
|
||||
Other = 0,
|
||||
Torso,
|
||||
Head,
|
||||
Arm,
|
||||
Hand,
|
||||
Leg,
|
||||
Foot
|
||||
}
|
||||
}
|
||||
110
Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs
Normal file
110
Content.Shared/GameObjects/Components/Body/Part/IBodyPart.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Surgery;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
public interface IBodyPart : IHasBody, IBodyPartContainer
|
||||
{
|
||||
new IBody? Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="BodyPartType"/> that this <see cref="IBodyPart"/> is considered
|
||||
/// to be.
|
||||
/// For example, <see cref="BodyPartType.Arm"/>.
|
||||
/// </summary>
|
||||
BodyPartType PartType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Plural version of this <see cref="IBodyPart"/> name.
|
||||
/// </summary>
|
||||
public string Plural { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines many things: how many mechanisms can be fit inside this
|
||||
/// <see cref="IBodyPart"/>, whether a body can fit through tiny crevices,
|
||||
/// etc.
|
||||
/// </summary>
|
||||
int Size { get; }
|
||||
|
||||
// TODO BODY Mechanisms occupying different parts at the body level
|
||||
/// <summary>
|
||||
/// Collection of all <see cref="IMechanism"/>s currently inside this
|
||||
/// <see cref="IBodyPart"/>.
|
||||
/// To add and remove from this list see <see cref="AddMechanism"/> and
|
||||
/// <see cref="RemoveMechanism"/>
|
||||
/// </summary>
|
||||
IReadOnlyCollection<IMechanism> Mechanisms { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If body part is vital
|
||||
/// </summary>
|
||||
public bool IsVital { get; }
|
||||
|
||||
public BodyPartSymmetry Symmetry { get; }
|
||||
|
||||
bool Drop();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given <see cref="SurgeryType"/> can be used on
|
||||
/// the current state of this <see cref="IBodyPart"/>.
|
||||
/// </summary>
|
||||
/// <returns>True if it can be used, false otherwise.</returns>
|
||||
bool SurgeryCheck(SurgeryType surgery);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to perform surgery on this <see cref="IBodyPart"/> with the given
|
||||
/// tool.
|
||||
/// </summary>
|
||||
/// <returns>True if successful, false if there was an error.</returns>
|
||||
public bool AttemptSurgery(SurgeryType toolType, IBodyPartContainer target, ISurgeon surgeon,
|
||||
IEntity performer);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if another <see cref="IBodyPart"/> can be connected to this one.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to connect.</param>
|
||||
/// <returns>True if it can be connected, false otherwise.</returns>
|
||||
bool CanAttachPart(IBodyPart part);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a <see cref="IMechanism"/> can be added on this
|
||||
/// <see cref="IBodyPart"/>.
|
||||
/// </summary>
|
||||
/// <returns>True if it can be added, false otherwise.</returns>
|
||||
bool CanAddMechanism(IMechanism mechanism);
|
||||
|
||||
bool TryAddMechanism(IMechanism mechanism, bool force = false);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to remove the given <see cref="mechanism"/> from this
|
||||
/// <see cref="IBodyPart"/>.
|
||||
/// </summary>
|
||||
/// <param name="mechanism">The mechanism to remove.</param>
|
||||
/// <returns>True if it was removed, false otherwise.</returns>
|
||||
bool RemoveMechanism(IMechanism mechanism);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to remove the given <see cref="mechanism"/> from this
|
||||
/// <see cref="IBodyPart"/> and drops it at the specified coordinates.
|
||||
/// </summary>
|
||||
/// <param name="mechanism">The mechanism to remove.</param>
|
||||
/// <param name="dropAt">The coordinates to drop it at.</param>
|
||||
/// <returns>True if it was removed, false otherwise.</returns>
|
||||
bool RemoveMechanism(IMechanism mechanism, EntityCoordinates dropAt);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to destroy the given <see cref="IMechanism"/> from
|
||||
/// this <see cref="IBodyPart"/>.
|
||||
/// The mechanism won't be deleted if it is not in this body part.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// True if the mechanism was in this body part and destroyed,
|
||||
/// false otherwise.
|
||||
/// </returns>
|
||||
bool DeleteMechanism(IMechanism mechanism);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when a body part
|
||||
/// is added to their owning entity.
|
||||
/// </summary>
|
||||
public interface IBodyPartAdded
|
||||
{
|
||||
void BodyPartAdded(BodyPartAddedEventArgs args);
|
||||
}
|
||||
|
||||
public class BodyPartAddedEventArgs : EventArgs
|
||||
{
|
||||
public BodyPartAddedEventArgs(IBodyPart part, string slot)
|
||||
{
|
||||
Part = part;
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
public IBodyPart Part { get; }
|
||||
|
||||
public string Slot { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
/// <summary>
|
||||
/// Making a class inherit from this interface allows you to do many
|
||||
/// things with it in the <see cref="SurgeryData"/> class.
|
||||
/// This includes passing it as an argument to a
|
||||
/// <see cref="SurgeryData.SurgeryAction"/> delegate, as to later typecast
|
||||
/// it back to the original class type.
|
||||
/// Every BodyPart also needs an <see cref="IBodyPartContainer"/> to be
|
||||
/// its parent (i.e. the <see cref="IBody"/> holds many
|
||||
/// <see cref="IBodyPart"/>s, each of which have an upward reference to it).
|
||||
/// </summary>
|
||||
// TODO BODY Remove
|
||||
public interface IBodyPartContainer
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when a body part
|
||||
/// is removed from their owning entity.
|
||||
/// </summary>
|
||||
public interface IBodyPartRemoved
|
||||
{
|
||||
void BodyPartRemoved(BodyPartRemovedEventArgs args);
|
||||
}
|
||||
|
||||
public class BodyPartRemovedEventArgs : EventArgs
|
||||
{
|
||||
public BodyPartRemovedEventArgs(IBodyPart part, string slot)
|
||||
{
|
||||
Part = part;
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
public IBodyPart Part { get; }
|
||||
|
||||
public string Slot { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part.Property
|
||||
{
|
||||
/// <summary>
|
||||
/// Property attachable to a <see cref="IBodyPart"/>.
|
||||
/// For example, this is used to define the speed capabilities of a
|
||||
/// leg. The movement system will look for a LegProperty on all BodyParts.
|
||||
/// </summary>
|
||||
public abstract class BodyPartPropertyComponent : Component, IBodyPartProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this property is currently active.
|
||||
/// </summary>
|
||||
public bool Active { get; set; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(this, b => b.Active, "active", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part.Property
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ExtensionComponent : BodyPartPropertyComponent
|
||||
{
|
||||
public override string Name => "Extension";
|
||||
|
||||
/// <summary>
|
||||
/// Current distance (in tiles).
|
||||
/// </summary>
|
||||
public float Distance { get; set; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(this, e => e.Distance, "distance", 3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part.Property
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an entity as being able to pick up items
|
||||
/// </summary>
|
||||
// TODO BODY Implement
|
||||
[RegisterComponent]
|
||||
public class GraspComponent : BodyPartPropertyComponent
|
||||
{
|
||||
public override string Name => "Grasp";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part.Property
|
||||
{
|
||||
public interface IBodyPartProperty : IComponent
|
||||
{
|
||||
bool Active { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part.Property
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class LegComponent : BodyPartPropertyComponent
|
||||
{
|
||||
public override string Name => "Leg";
|
||||
|
||||
/// <summary>
|
||||
/// Speed (in tiles per second).
|
||||
/// </summary>
|
||||
public float Speed { get; set; }
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(this, l => l.Speed, "speed", 2.6f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Surgery;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Part
|
||||
{
|
||||
public abstract class SharedBodyPartComponent : Component, IBodyPart
|
||||
{
|
||||
public override string Name => "BodyPart";
|
||||
|
||||
public override uint? NetID => ContentNetIDs.BODY_PART;
|
||||
|
||||
private IBody? _body;
|
||||
|
||||
// TODO BODY Remove
|
||||
private List<string> _mechanismIds = new List<string>();
|
||||
public IReadOnlyList<string> MechanismIds => _mechanismIds;
|
||||
|
||||
[ViewVariables]
|
||||
private HashSet<IMechanism> _mechanisms = new HashSet<IMechanism>();
|
||||
|
||||
[ViewVariables]
|
||||
public IBody? Body
|
||||
{
|
||||
get => _body;
|
||||
set
|
||||
{
|
||||
if (_body == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var old = _body;
|
||||
_body = value;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
foreach (var mechanism in _mechanisms)
|
||||
{
|
||||
mechanism.OnBodyAdd(old, value);
|
||||
}
|
||||
}
|
||||
else if (old != null)
|
||||
{
|
||||
foreach (var mechanism in _mechanisms)
|
||||
{
|
||||
mechanism.OnBodyRemove(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables] public BodyPartType PartType { get; private set; }
|
||||
|
||||
[ViewVariables] public string Plural { get; private set; } = string.Empty;
|
||||
|
||||
[ViewVariables] public int Size { get; private set; }
|
||||
|
||||
[ViewVariables] public int SizeUsed { get; private set; }
|
||||
|
||||
// TODO BODY size used
|
||||
// TODO BODY surgerydata
|
||||
|
||||
/// <summary>
|
||||
/// What types of BodyParts this <see cref="IBodyPart"/> can easily attach to.
|
||||
/// For the most part, most limbs aren't universal and require extra work to
|
||||
/// attach between types.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public BodyPartCompatibility Compatibility { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set of all <see cref="IMechanism"/> currently inside this
|
||||
/// <see cref="IBodyPart"/>.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public IReadOnlyCollection<IMechanism> Mechanisms => _mechanisms;
|
||||
|
||||
// TODO BODY Replace with a simulation of organs
|
||||
/// <summary>
|
||||
/// Represents if body part is vital for creature.
|
||||
/// If the last vital body part is removed creature dies
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool IsVital { get; private set; }
|
||||
|
||||
[ViewVariables]
|
||||
public BodyPartSymmetry Symmetry { get; private set; }
|
||||
|
||||
// TODO BODY
|
||||
[ViewVariables]
|
||||
public SurgeryDataComponent? SurgeryDataComponent => Owner.GetComponentOrNull<SurgeryDataComponent>();
|
||||
|
||||
protected virtual void OnAddMechanism(IMechanism mechanism)
|
||||
{
|
||||
var prototypeId = mechanism.Owner.Prototype!.ID;
|
||||
|
||||
if (!_mechanismIds.Contains(prototypeId))
|
||||
{
|
||||
_mechanismIds.Add(prototypeId);
|
||||
}
|
||||
|
||||
mechanism.Part = this;
|
||||
SizeUsed += mechanism.Size;
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
protected virtual void OnRemoveMechanism(IMechanism mechanism)
|
||||
{
|
||||
_mechanismIds.Remove(mechanism.Owner.Prototype!.ID);
|
||||
mechanism.Part = null;
|
||||
SizeUsed -= mechanism.Size;
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
// TODO BODY serialize any changed properties?
|
||||
|
||||
serializer.DataField(this, b => b.PartType, "partType", BodyPartType.Other);
|
||||
|
||||
serializer.DataField(this, b => b.Plural, "plural", string.Empty);
|
||||
|
||||
serializer.DataField(this, b => b.Size, "size", 1);
|
||||
|
||||
serializer.DataField(this, b => b.Compatibility, "compatibility", BodyPartCompatibility.Universal);
|
||||
|
||||
serializer.DataField(this, b => b.IsVital, "vital", false);
|
||||
|
||||
serializer.DataField(this, b => b.Symmetry, "symmetry", BodyPartSymmetry.None);
|
||||
|
||||
serializer.DataField(ref _mechanismIds, "mechanisms", new List<string>());
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
var mechanismIds = new EntityUid[_mechanisms.Count];
|
||||
|
||||
var i = 0;
|
||||
foreach (var mechanism in _mechanisms)
|
||||
{
|
||||
mechanismIds[i] = mechanism.Owner.Uid;
|
||||
i++;
|
||||
}
|
||||
|
||||
return new BodyPartComponentState(mechanismIds);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (!(curState is BodyPartComponentState state))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newMechanisms = state.Mechanisms();
|
||||
|
||||
foreach (var mechanism in _mechanisms.ToArray())
|
||||
{
|
||||
if (!newMechanisms.Contains(mechanism))
|
||||
{
|
||||
RemoveMechanism(mechanism);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var mechanism in newMechanisms)
|
||||
{
|
||||
if (!_mechanisms.Contains(mechanism))
|
||||
{
|
||||
TryAddMechanism(mechanism, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Drop()
|
||||
{
|
||||
Body = null;
|
||||
Owner.Transform.AttachToGridOrMap();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SurgeryCheck(SurgeryType surgery)
|
||||
{
|
||||
return SurgeryDataComponent?.CheckSurgery(surgery) ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to perform surgery on this <see cref="IBodyPart"/> with the given
|
||||
/// tool.
|
||||
/// </summary>
|
||||
/// <returns>True if successful, false if there was an error.</returns>
|
||||
public bool AttemptSurgery(SurgeryType toolType, IBodyPartContainer target, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
DebugTools.AssertNotNull(toolType);
|
||||
DebugTools.AssertNotNull(target);
|
||||
DebugTools.AssertNotNull(surgeon);
|
||||
DebugTools.AssertNotNull(performer);
|
||||
|
||||
return SurgeryDataComponent?.PerformSurgery(toolType, target, surgeon, performer) ?? false;
|
||||
}
|
||||
|
||||
public bool CanAttachPart(IBodyPart part)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
return SurgeryDataComponent?.CanAttachBodyPart(part) ?? false;
|
||||
}
|
||||
|
||||
public bool CanAddMechanism(IMechanism mechanism)
|
||||
{
|
||||
DebugTools.AssertNotNull(mechanism);
|
||||
|
||||
return SurgeryDataComponent != null &&
|
||||
SizeUsed + mechanism.Size <= Size &&
|
||||
SurgeryDataComponent.CanAddMechanism(mechanism);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to add a mechanism onto this body part.
|
||||
/// </summary>
|
||||
/// <param name="mechanism">The mechanism to try to add.</param>
|
||||
/// <param name="force">
|
||||
/// Whether or not to check if the mechanism can be added.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// True if successful, false if there was an error
|
||||
/// (e.g. not enough room in <see cref="IBodyPart"/>).
|
||||
/// Will return false even when forced if the mechanism is already
|
||||
/// added in this <see cref="IBodyPart"/>.
|
||||
/// </returns>
|
||||
public bool TryAddMechanism(IMechanism mechanism, bool force = false)
|
||||
{
|
||||
DebugTools.AssertNotNull(mechanism);
|
||||
|
||||
if (!force && !CanAddMechanism(mechanism))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_mechanisms.Add(mechanism))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnAddMechanism(mechanism);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveMechanism(IMechanism mechanism)
|
||||
{
|
||||
DebugTools.AssertNotNull(mechanism);
|
||||
|
||||
if (!_mechanisms.Remove(mechanism))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OnRemoveMechanism(mechanism);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveMechanism(IMechanism mechanism, EntityCoordinates coordinates)
|
||||
{
|
||||
if (RemoveMechanism(mechanism))
|
||||
{
|
||||
mechanism.Owner.Transform.Coordinates = coordinates;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool DeleteMechanism(IMechanism mechanism)
|
||||
{
|
||||
DebugTools.AssertNotNull(mechanism);
|
||||
|
||||
if (!RemoveMechanism(mechanism))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
mechanism.Owner.Delete();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class BodyPartComponentState : ComponentState
|
||||
{
|
||||
private List<IMechanism>? _mechanisms;
|
||||
|
||||
public readonly EntityUid[] MechanismIds;
|
||||
|
||||
public BodyPartComponentState(EntityUid[] mechanismIds) : base(ContentNetIDs.BODY_PART)
|
||||
{
|
||||
MechanismIds = mechanismIds;
|
||||
}
|
||||
|
||||
public List<IMechanism> Mechanisms(IEntityManager? entityManager = null)
|
||||
{
|
||||
if (_mechanisms != null)
|
||||
{
|
||||
return _mechanisms;
|
||||
}
|
||||
|
||||
entityManager ??= IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var mechanisms = new List<IMechanism>(MechanismIds.Length);
|
||||
|
||||
foreach (var id in MechanismIds)
|
||||
{
|
||||
if (!entityManager.TryGetEntity(id, out var entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entity.TryGetComponent(out IMechanism? mechanism))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
mechanisms.Add(mechanism);
|
||||
}
|
||||
|
||||
return _mechanisms = mechanisms;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Preset
|
||||
{
|
||||
/// <summary>
|
||||
/// Prototype for the BodyPreset class.
|
||||
/// </summary>
|
||||
[Prototype("bodyPreset")]
|
||||
[Serializable, NetSerializable]
|
||||
public class BodyPresetPrototype : IPrototype, IIndexedPrototype
|
||||
{
|
||||
private string _id;
|
||||
private string _name;
|
||||
private Dictionary<string, string> _partIDs;
|
||||
|
||||
[ViewVariables] public string ID => _id;
|
||||
|
||||
[ViewVariables] public string Name => _name;
|
||||
|
||||
[ViewVariables] public Dictionary<string, string> PartIDs => new Dictionary<string, string>(_partIDs);
|
||||
|
||||
public virtual void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
var serializer = YamlObjectSerializer.NewReader(mapping);
|
||||
serializer.DataField(ref _id, "id", string.Empty);
|
||||
serializer.DataField(ref _name, "name", string.Empty);
|
||||
serializer.DataField(ref _partIDs, "partIDs", new Dictionary<string, string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components.UserInterface;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Scanner
|
||||
{
|
||||
public abstract class SharedBodyScannerComponent : Component
|
||||
{
|
||||
public override string Name => "BodyScanner";
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum BodyScannerUiKey
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class BodyScannerUIState : BoundUserInterfaceState
|
||||
{
|
||||
public readonly EntityUid Uid;
|
||||
|
||||
public BodyScannerUIState(EntityUid uid)
|
||||
{
|
||||
Uid = uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,753 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Body.Part.Property;
|
||||
using Content.Shared.GameObjects.Components.Body.Preset;
|
||||
using Content.Shared.GameObjects.Components.Body.Template;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body
|
||||
{
|
||||
// TODO BODY Damage methods for collections of IDamageableComponents
|
||||
public abstract class SharedBodyComponent : Component, IBody
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override string Name => "Body";
|
||||
|
||||
public override uint? NetID => ContentNetIDs.BODY;
|
||||
|
||||
private string? _centerSlot;
|
||||
|
||||
private Dictionary<string, string> _partIds = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, IBodyPart> _parts = new Dictionary<string, IBodyPart>();
|
||||
|
||||
[ViewVariables] public string? TemplateName { get; private set; }
|
||||
|
||||
[ViewVariables] public string? PresetName { get; private set; }
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<string, BodyPartType> Slots { get; private set; } = new Dictionary<string, BodyPartType>();
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<string, List<string>> Connections { get; private set; } = new Dictionary<string, List<string>>();
|
||||
|
||||
/// <summary>
|
||||
/// Maps slots to the part filling each one.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public IReadOnlyDictionary<string, IBodyPart> Parts => _parts;
|
||||
|
||||
public IReadOnlyDictionary<string, string> PartIds => _partIds;
|
||||
|
||||
[ViewVariables] public IReadOnlyDictionary<string, string> PartIDs => _partIds;
|
||||
|
||||
protected virtual bool CanAddPart(string slot, IBodyPart part)
|
||||
{
|
||||
if (!HasSlot(slot) || !_parts.TryAdd(slot, part))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void OnAddPart(string slot, IBodyPart part)
|
||||
{
|
||||
part.Owner.Transform.AttachParent(Owner);
|
||||
part.Body = this;
|
||||
|
||||
var argsAdded = new BodyPartAddedEventArgs(part, slot);
|
||||
|
||||
foreach (var component in Owner.GetAllComponents<IBodyPartAdded>().ToArray())
|
||||
{
|
||||
component.BodyPartAdded(argsAdded);
|
||||
}
|
||||
|
||||
// TODO BODY Sort this duplicate out
|
||||
OnBodyChanged();
|
||||
}
|
||||
|
||||
protected virtual void OnRemovePart(string slot, IBodyPart part)
|
||||
{
|
||||
// TODO BODY Move to Body part
|
||||
if (!part.Owner.Transform.Deleted)
|
||||
{
|
||||
part.Owner.Transform.AttachToGridOrMap();
|
||||
}
|
||||
|
||||
part.Body = null;
|
||||
|
||||
var args = new BodyPartRemovedEventArgs(part, slot);
|
||||
|
||||
foreach (var component in Owner.GetAllComponents<IBodyPartRemoved>())
|
||||
{
|
||||
component.BodyPartRemoved(args);
|
||||
}
|
||||
|
||||
// creadth: fall down if no legs
|
||||
if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0)
|
||||
{
|
||||
EntitySystem.Get<SharedStandingStateSystem>().Down(Owner);
|
||||
}
|
||||
|
||||
// creadth: immediately kill entity if last vital part removed
|
||||
if (Owner.TryGetComponent(out IDamageableComponent? damageable))
|
||||
{
|
||||
if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0)
|
||||
{
|
||||
damageable.CurrentState = DamageState.Dead;
|
||||
damageable.ForceHealthChangedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
OnBodyChanged();
|
||||
}
|
||||
|
||||
public bool TryAddPart(string slot, IBodyPart part, bool force = false)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
DebugTools.AssertNotNull(slot);
|
||||
|
||||
if (force)
|
||||
{
|
||||
if (!HasSlot(slot))
|
||||
{
|
||||
Slots[slot] = part.PartType;
|
||||
}
|
||||
|
||||
_parts[slot] = part;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!CanAddPart(slot, part))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
OnAddPart(slot, part);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HasPart(string slot)
|
||||
{
|
||||
DebugTools.AssertNotNull(slot);
|
||||
|
||||
return _parts.ContainsKey(slot);
|
||||
}
|
||||
|
||||
public void RemovePart(IBodyPart part, bool drop)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
var slotName = _parts.FirstOrDefault(x => x.Value == part).Key;
|
||||
|
||||
if (string.IsNullOrEmpty(slotName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RemovePart(slotName, drop);
|
||||
}
|
||||
|
||||
// TODO BODY invert this behavior with the one above
|
||||
public bool RemovePart(string slot, bool drop)
|
||||
{
|
||||
DebugTools.AssertNotNull(slot);
|
||||
|
||||
if (!_parts.Remove(slot, out var part))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (drop)
|
||||
{
|
||||
part.Drop();
|
||||
}
|
||||
|
||||
OnRemovePart(slot, part);
|
||||
|
||||
if (TryGetSlotConnections(slot, out var connections))
|
||||
{
|
||||
foreach (var connectionName in connections)
|
||||
{
|
||||
if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result))
|
||||
{
|
||||
RemovePart(connectionName, drop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slotName)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part);
|
||||
|
||||
if (pair.Equals(default))
|
||||
{
|
||||
slotName = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RemovePart(pair.Key, false))
|
||||
{
|
||||
slotName = pair.Key;
|
||||
return true;
|
||||
}
|
||||
|
||||
slotName = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryDropPart(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart>? dropped)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
if (!_parts.ContainsValue(part))
|
||||
{
|
||||
dropped = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RemovePart(part, out var slotName))
|
||||
{
|
||||
dropped = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
part.Drop();
|
||||
|
||||
dropped = new List<IBodyPart> {part};
|
||||
// Call disconnect on all limbs that were hanging off this limb.
|
||||
if (TryGetSlotConnections(slotName, out var connections))
|
||||
{
|
||||
// TODO BODY optimize
|
||||
foreach (var connectionName in connections)
|
||||
{
|
||||
if (TryGetPart(connectionName, out var result) &&
|
||||
!ConnectedToCenter(result) &&
|
||||
RemovePart(connectionName, true))
|
||||
{
|
||||
dropped.Add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnBodyChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ConnectedToCenter(IBodyPart part)
|
||||
{
|
||||
var searchedSlots = new List<string>();
|
||||
|
||||
return TryGetSlot(part, out var result) &&
|
||||
ConnectedToCenterPartRecursion(searchedSlots, result);
|
||||
}
|
||||
|
||||
private bool ConnectedToCenterPartRecursion(ICollection<string> searchedSlots, string slotName)
|
||||
{
|
||||
if (!TryGetPart(slotName, out var part))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (part == CenterPart())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
searchedSlots.Add(slotName);
|
||||
|
||||
if (!TryGetSlotConnections(slotName, out var connections))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
if (!searchedSlots.Contains(connection) &&
|
||||
ConnectedToCenterPartRecursion(searchedSlots, connection))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IBodyPart? CenterPart()
|
||||
{
|
||||
if (_centerSlot == null) return null;
|
||||
|
||||
return Parts.GetValueOrDefault(_centerSlot);
|
||||
}
|
||||
|
||||
public bool HasSlot(string slot)
|
||||
{
|
||||
return Slots.ContainsKey(slot);
|
||||
}
|
||||
|
||||
public bool TryGetPart(string slot, [NotNullWhen(true)] out IBodyPart? result)
|
||||
{
|
||||
return Parts.TryGetValue(slot, out result);
|
||||
}
|
||||
|
||||
public bool TryGetSlot(IBodyPart part, [NotNullWhen(true)] out string? slot)
|
||||
{
|
||||
// We enforce that there is only one of each value in the dictionary,
|
||||
// so we can iterate through the dictionary values to get the key from there.
|
||||
var pair = Parts.FirstOrDefault(x => x.Value == part);
|
||||
slot = pair.Key;
|
||||
|
||||
return !pair.Equals(default);
|
||||
}
|
||||
|
||||
public bool TryGetSlotType(string slot, out BodyPartType result)
|
||||
{
|
||||
return Slots.TryGetValue(slot, out result);
|
||||
}
|
||||
|
||||
public bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List<string>? connections)
|
||||
{
|
||||
return Connections.TryGetValue(slot, out connections);
|
||||
}
|
||||
|
||||
public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List<IBodyPart>? connections)
|
||||
{
|
||||
if (!Connections.TryGetValue(slot, out var slotConnections))
|
||||
{
|
||||
connections = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
connections = new List<IBodyPart>();
|
||||
foreach (var connection in slotConnections)
|
||||
{
|
||||
if (TryGetPart(connection, out var part))
|
||||
{
|
||||
connections.Add(part);
|
||||
}
|
||||
}
|
||||
|
||||
if (connections.Count <= 0)
|
||||
{
|
||||
connections = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetPartConnections(IBodyPart part, [NotNullWhen(true)] out List<IBodyPart>? connections)
|
||||
{
|
||||
connections = null;
|
||||
|
||||
return TryGetSlot(part, out var slotName) &&
|
||||
TryGetPartConnections(slotName, out connections);
|
||||
}
|
||||
|
||||
public List<IBodyPart> GetPartsOfType(BodyPartType type)
|
||||
{
|
||||
var parts = new List<IBodyPart>();
|
||||
|
||||
foreach (var part in Parts.Values)
|
||||
{
|
||||
if (part.PartType == type)
|
||||
{
|
||||
parts.Add(part);
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public List<(IBodyPart part, IBodyPartProperty property)> GetPartsWithProperty(Type type)
|
||||
{
|
||||
var parts = new List<(IBodyPart, IBodyPartProperty)>();
|
||||
|
||||
foreach (var part in Parts.Values)
|
||||
{
|
||||
if (part.TryGetProperty(type, out var property))
|
||||
{
|
||||
parts.Add((part, property));
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
public List<(IBodyPart part, T property)> GetPartsWithProperty<T>() where T : class, IBodyPartProperty
|
||||
{
|
||||
var parts = new List<(IBodyPart, T)>();
|
||||
|
||||
foreach (var part in Parts.Values)
|
||||
{
|
||||
if (part.TryGetProperty<T>(out var property))
|
||||
{
|
||||
parts.Add((part, property));
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private void CalculateSpeed()
|
||||
{
|
||||
if (!Owner.TryGetComponent(out MovementSpeedModifierComponent? playerMover))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var legs = GetPartsWithProperty<LegComponent>();
|
||||
float speedSum = 0;
|
||||
|
||||
foreach (var leg in GetPartsWithProperty<LegComponent>())
|
||||
{
|
||||
var footDistance = DistanceToNearestFoot(leg.part);
|
||||
|
||||
if (Math.Abs(footDistance - float.MinValue) <= 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
speedSum += leg.property.Speed * (1 + (float) Math.Log(footDistance, 1024.0));
|
||||
}
|
||||
|
||||
if (speedSum <= 0.001f)
|
||||
{
|
||||
playerMover.BaseWalkSpeed = 0.8f;
|
||||
playerMover.BaseSprintSpeed = 2.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extra legs stack diminishingly.
|
||||
playerMover.BaseWalkSpeed =
|
||||
speedSum / (legs.Count - (float) Math.Log(legs.Count, 4.0));
|
||||
|
||||
playerMover.BaseSprintSpeed = playerMover.BaseWalkSpeed * 1.75f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the layout of this body changes.
|
||||
/// </summary>
|
||||
private void OnBodyChanged()
|
||||
{
|
||||
// Calculate move speed based on this body.
|
||||
if (Owner.HasComponent<MovementSpeedModifierComponent>())
|
||||
{
|
||||
CalculateSpeed();
|
||||
}
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combined length of the distance to the nearest
|
||||
/// <see cref="IBodyPart"/> that is a foot.
|
||||
/// If you consider a <see cref="IBody"/> a node map, then it will
|
||||
/// look for a foot node from the given node. It can only search
|
||||
/// through <see cref="IBodyPart"/>s with an
|
||||
/// <see cref="ExtensionComponent"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The distance to the foot if found, <see cref="float.MinValue"/>
|
||||
/// otherwise.
|
||||
/// </returns>
|
||||
public float DistanceToNearestFoot(IBodyPart source)
|
||||
{
|
||||
if (source.PartType == BodyPartType.Foot &&
|
||||
source.TryGetProperty<ExtensionComponent>(out var extension))
|
||||
{
|
||||
return extension.Distance;
|
||||
}
|
||||
|
||||
return LookForFootRecursion(source, new List<IBodyPart>());
|
||||
}
|
||||
|
||||
private float LookForFootRecursion(IBodyPart current, ICollection<IBodyPart> searchedParts)
|
||||
{
|
||||
if (!current.TryGetProperty<ExtensionComponent>(out var extProperty))
|
||||
{
|
||||
return float.MinValue;
|
||||
}
|
||||
|
||||
// Get all connected parts if the current part has an extension property
|
||||
if (!TryGetPartConnections(current, out var connections))
|
||||
{
|
||||
return float.MinValue;
|
||||
}
|
||||
|
||||
// If a connected BodyPart is a foot, return this BodyPart's length.
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
if (connection.PartType == BodyPartType.Foot &&
|
||||
!searchedParts.Contains(connection))
|
||||
{
|
||||
return extProperty.Distance;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, get the recursion values of all connected BodyParts and
|
||||
// store them in a list.
|
||||
var distances = new List<float>();
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
if (!searchedParts.Contains(connection))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = LookForFootRecursion(connection, searchedParts);
|
||||
|
||||
if (Math.Abs(result - float.MinValue) > 0.001f)
|
||||
{
|
||||
distances.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
// If one or more of the searches found a foot, return the smallest one
|
||||
// and add this ones length.
|
||||
if (distances.Count > 0)
|
||||
{
|
||||
return distances.Min<float>() + extProperty.Distance;
|
||||
}
|
||||
|
||||
return float.MinValue;
|
||||
}
|
||||
|
||||
// TODO BODY optimize this
|
||||
public KeyValuePair<string, BodyPartType> SlotAt(int index)
|
||||
{
|
||||
return Slots.ElementAt(index);
|
||||
}
|
||||
|
||||
public KeyValuePair<string, IBodyPart> PartAt(int index)
|
||||
{
|
||||
return Parts.ElementAt(index);
|
||||
}
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataReadWriteFunction(
|
||||
"template",
|
||||
null,
|
||||
name =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var template = _prototypeManager.Index<BodyTemplatePrototype>(name);
|
||||
|
||||
Connections = template.Connections;
|
||||
Slots = template.Slots;
|
||||
_centerSlot = template.CenterSlot;
|
||||
|
||||
TemplateName = name;
|
||||
},
|
||||
() => TemplateName);
|
||||
|
||||
serializer.DataReadWriteFunction(
|
||||
"preset",
|
||||
null,
|
||||
name =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var preset = _prototypeManager.Index<BodyPresetPrototype>(name);
|
||||
|
||||
_partIds = preset.PartIDs;
|
||||
},
|
||||
() => PresetName);
|
||||
|
||||
serializer.DataReadWriteFunction(
|
||||
"connections",
|
||||
new Dictionary<string, List<string>>(),
|
||||
connections =>
|
||||
{
|
||||
foreach (var (from, to) in connections)
|
||||
{
|
||||
Connections.GetOrNew(from).AddRange(to);
|
||||
}
|
||||
},
|
||||
() => Connections);
|
||||
|
||||
serializer.DataReadWriteFunction(
|
||||
"slots",
|
||||
new Dictionary<string, BodyPartType>(),
|
||||
slots =>
|
||||
{
|
||||
foreach (var (part, type) in slots)
|
||||
{
|
||||
Slots[part] = type;
|
||||
}
|
||||
},
|
||||
() => Slots);
|
||||
|
||||
// TODO BODY Move to template or somewhere else
|
||||
serializer.DataReadWriteFunction(
|
||||
"centerSlot",
|
||||
null,
|
||||
slot => _centerSlot = slot,
|
||||
() => _centerSlot);
|
||||
|
||||
serializer.DataReadWriteFunction(
|
||||
"partIds",
|
||||
new Dictionary<string, string>(),
|
||||
partIds =>
|
||||
{
|
||||
foreach (var (slot, part) in partIds)
|
||||
{
|
||||
_partIds[slot] = part;
|
||||
}
|
||||
},
|
||||
() => _partIds);
|
||||
|
||||
// Our prototypes don't force the user to define a BodyPart connection twice. E.g. Head: Torso v.s. Torso: Head.
|
||||
// The user only has to do one. We want it to be that way in the code, though, so this cleans that up.
|
||||
var cleanedConnections = new Dictionary<string, List<string>>();
|
||||
foreach (var targetSlotName in Slots.Keys)
|
||||
{
|
||||
var tempConnections = new List<string>();
|
||||
foreach (var (slotName, slotConnections) in Connections)
|
||||
{
|
||||
if (slotName == targetSlotName)
|
||||
{
|
||||
foreach (var connection in slotConnections)
|
||||
{
|
||||
if (!tempConnections.Contains(connection))
|
||||
{
|
||||
tempConnections.Add(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (slotConnections.Contains(targetSlotName))
|
||||
{
|
||||
tempConnections.Add(slotName);
|
||||
}
|
||||
}
|
||||
|
||||
if (tempConnections.Count > 0)
|
||||
{
|
||||
cleanedConnections.Add(targetSlotName, tempConnections);
|
||||
}
|
||||
}
|
||||
|
||||
Connections = cleanedConnections;
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
var parts = new (string slot, EntityUid partId)[_parts.Count];
|
||||
|
||||
var i = 0;
|
||||
foreach (var (slot, part) in _parts)
|
||||
{
|
||||
parts[i] = (slot, part.Owner.Uid);
|
||||
i++;
|
||||
}
|
||||
|
||||
return new BodyComponentState(parts);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (!(curState is BodyComponentState state))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newParts = state.Parts();
|
||||
|
||||
foreach (var (slot, oldPart) in _parts)
|
||||
{
|
||||
if (!newParts.TryGetValue(slot, out var newPart) ||
|
||||
newPart != oldPart)
|
||||
{
|
||||
RemovePart(oldPart, false);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (slot, newPart) in newParts)
|
||||
{
|
||||
if (!_parts.TryGetValue(slot, out var oldPart) ||
|
||||
oldPart != newPart)
|
||||
{
|
||||
TryAddPart(slot, newPart, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class BodyComponentState : ComponentState
|
||||
{
|
||||
private Dictionary<string, IBodyPart>? _parts;
|
||||
|
||||
public readonly (string slot, EntityUid partId)[] PartIds;
|
||||
|
||||
public BodyComponentState((string slot, EntityUid partId)[] partIds) : base(ContentNetIDs.BODY)
|
||||
{
|
||||
PartIds = partIds;
|
||||
}
|
||||
|
||||
public Dictionary<string, IBodyPart> Parts(IEntityManager? entityManager = null)
|
||||
{
|
||||
if (_parts != null)
|
||||
{
|
||||
return _parts;
|
||||
}
|
||||
|
||||
entityManager ??= IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var parts = new Dictionary<string, IBodyPart>(PartIds.Length);
|
||||
|
||||
foreach (var (slot, partId) in PartIds)
|
||||
{
|
||||
if (!entityManager.TryGetEntity(partId, out var entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entity.TryGetComponent(out IBodyPart? part))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
parts[slot] = part;
|
||||
}
|
||||
|
||||
return _parts = parts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body
|
||||
{
|
||||
public abstract class SharedBodyManagerComponent : DamageableComponent, ISharedBodyManagerComponent
|
||||
{
|
||||
public override string Name => "BodyManager";
|
||||
|
||||
public override uint? NetID => ContentNetIDs.BODY_MANAGER;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BodyPartAddedMessage : ComponentMessage
|
||||
{
|
||||
public readonly string RSIPath;
|
||||
public readonly string RSIState;
|
||||
public readonly Enum RSIMap;
|
||||
|
||||
public BodyPartAddedMessage(string rsiPath, string rsiState, Enum rsiMap)
|
||||
{
|
||||
Directed = true;
|
||||
RSIPath = rsiPath;
|
||||
RSIState = rsiState;
|
||||
RSIMap = rsiMap;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class BodyPartRemovedMessage : ComponentMessage
|
||||
{
|
||||
public readonly Enum RSIMap;
|
||||
public readonly EntityUid? Dropped;
|
||||
|
||||
public BodyPartRemovedMessage(Enum rsiMap, EntityUid? dropped = null)
|
||||
{
|
||||
Directed = true;
|
||||
RSIMap = rsiMap;
|
||||
Dropped = dropped;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MechanismSpriteAddedMessage : ComponentMessage
|
||||
{
|
||||
public readonly Enum RSIMap;
|
||||
|
||||
public MechanismSpriteAddedMessage(Enum rsiMap)
|
||||
{
|
||||
Directed = true;
|
||||
RSIMap = rsiMap;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MechanismSpriteRemovedMessage : ComponentMessage
|
||||
{
|
||||
public readonly Enum RSIMap;
|
||||
|
||||
public MechanismSpriteRemovedMessage(Enum rsiMap)
|
||||
{
|
||||
Directed = true;
|
||||
RSIMap = rsiMap;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine whether a BodyPart can connect to another BodyPart.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum BodyPartCompatibility
|
||||
{
|
||||
Universal = 0,
|
||||
Biological,
|
||||
Mechanical
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each BodyPart has a BodyPartType used to determine a variety of things.
|
||||
/// For instance, what slots it can fit into.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum BodyPartType
|
||||
{
|
||||
Other = 0,
|
||||
Torso,
|
||||
Head,
|
||||
Arm,
|
||||
Hand,
|
||||
Leg,
|
||||
Foot
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a surgery operation that can be performed.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum SurgeryType
|
||||
{
|
||||
None = 0,
|
||||
Incision,
|
||||
Retraction,
|
||||
Cauterization,
|
||||
VesselCompression,
|
||||
Drilling,
|
||||
Amputation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
/// <summary>
|
||||
/// Data class representing the surgery state of a biological entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SurgeryDataComponent))]
|
||||
public class BiologicalSurgeryDataComponent : SurgeryDataComponent
|
||||
{
|
||||
public override string Name => "BiologicalSurgeryData";
|
||||
|
||||
private readonly List<IMechanism> _disconnectedOrgans = new List<IMechanism>();
|
||||
|
||||
private bool _skinOpened;
|
||||
private bool _skinRetracted;
|
||||
private bool _vesselsClamped;
|
||||
|
||||
protected override SurgeryAction? GetSurgeryStep(SurgeryType toolType)
|
||||
{
|
||||
if (Parent == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (toolType == SurgeryType.Amputation)
|
||||
{
|
||||
return RemoveBodyPartSurgery;
|
||||
}
|
||||
|
||||
if (!_skinOpened)
|
||||
{
|
||||
// Case: skin is normal.
|
||||
if (toolType == SurgeryType.Incision)
|
||||
{
|
||||
return OpenSkinSurgery;
|
||||
}
|
||||
}
|
||||
else if (!_vesselsClamped)
|
||||
{
|
||||
// Case: skin is opened, but not clamped.
|
||||
switch (toolType)
|
||||
{
|
||||
case SurgeryType.VesselCompression:
|
||||
return ClampVesselsSurgery;
|
||||
case SurgeryType.Cauterization:
|
||||
return CauterizeIncisionSurgery;
|
||||
}
|
||||
}
|
||||
else if (!_skinRetracted)
|
||||
{
|
||||
// Case: skin is opened and clamped, but not retracted.
|
||||
switch (toolType)
|
||||
{
|
||||
case SurgeryType.Retraction:
|
||||
return RetractSkinSurgery;
|
||||
case SurgeryType.Cauterization:
|
||||
return CauterizeIncisionSurgery;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case: skin is fully open.
|
||||
if (Parent.Mechanisms.Count > 0 &&
|
||||
toolType == SurgeryType.VesselCompression)
|
||||
{
|
||||
if (_disconnectedOrgans.Except(Parent.Mechanisms).Count() != 0 ||
|
||||
Parent.Mechanisms.Except(_disconnectedOrgans).Count() != 0)
|
||||
{
|
||||
return LoosenOrganSurgery;
|
||||
}
|
||||
}
|
||||
|
||||
if (_disconnectedOrgans.Count > 0 && toolType == SurgeryType.Incision)
|
||||
{
|
||||
return RemoveOrganSurgery;
|
||||
}
|
||||
|
||||
if (toolType == SurgeryType.Cauterization)
|
||||
{
|
||||
return CauterizeIncisionSurgery;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string GetDescription(IEntity target)
|
||||
{
|
||||
if (Parent == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var toReturn = "";
|
||||
|
||||
if (_skinOpened && !_vesselsClamped)
|
||||
{
|
||||
// Case: skin is opened, but not clamped.
|
||||
toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is prone to bleeding.\n",
|
||||
target, Parent.Name);
|
||||
}
|
||||
else if (_skinOpened && _vesselsClamped && !_skinRetracted)
|
||||
{
|
||||
// Case: skin is opened and clamped, but not retracted.
|
||||
toReturn += Loc.GetString("The skin on {0:their} {1} has an incision, but it is not retracted.\n",
|
||||
target, Parent.Name);
|
||||
}
|
||||
else if (_skinOpened && _vesselsClamped && _skinRetracted)
|
||||
{
|
||||
// Case: skin is fully open.
|
||||
toReturn += Loc.GetString("There is an incision on {0:their} {1}.\n", target, Parent.Name);
|
||||
foreach (var mechanism in _disconnectedOrgans)
|
||||
{
|
||||
toReturn += Loc.GetString("{0:their} {1} is loose.\n", target, mechanism.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public override bool CanAddMechanism(IMechanism mechanism)
|
||||
{
|
||||
return Parent != null &&
|
||||
_skinOpened &&
|
||||
_vesselsClamped &&
|
||||
_skinRetracted;
|
||||
}
|
||||
|
||||
public override bool CanAttachBodyPart(IBodyPart part)
|
||||
{
|
||||
return Parent != null;
|
||||
// TODO BODY if a part is disconnected, you should have to do some surgery to allow another bodypart to be attached.
|
||||
}
|
||||
|
||||
private void OpenSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Cut open the skin..."));
|
||||
|
||||
// TODO BODY do_after: Delay
|
||||
_skinOpened = true;
|
||||
}
|
||||
|
||||
private void ClampVesselsSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Clamp the vessels..."));
|
||||
|
||||
// TODO BODY do_after: Delay
|
||||
_vesselsClamped = true;
|
||||
}
|
||||
|
||||
private void RetractSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Retract the skin..."));
|
||||
|
||||
// TODO BODY do_after: Delay
|
||||
_skinRetracted = true;
|
||||
}
|
||||
|
||||
private void CauterizeIncisionSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Cauterize the incision..."));
|
||||
|
||||
// TODO BODY do_after: Delay
|
||||
_skinOpened = false;
|
||||
_vesselsClamped = false;
|
||||
_skinRetracted = false;
|
||||
}
|
||||
|
||||
private void LoosenOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
if (Parent.Mechanisms.Count <= 0) return;
|
||||
|
||||
var toSend = new List<IMechanism>();
|
||||
foreach (var mechanism in Parent.Mechanisms)
|
||||
{
|
||||
if (!_disconnectedOrgans.Contains(mechanism))
|
||||
{
|
||||
toSend.Add(mechanism);
|
||||
}
|
||||
}
|
||||
|
||||
if (toSend.Count > 0)
|
||||
{
|
||||
surgeon.RequestMechanism(toSend, LoosenOrganSurgeryCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoosenOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon,
|
||||
IEntity performer)
|
||||
{
|
||||
if (Parent == null || target == null || !Parent.Mechanisms.Contains(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Loosen the organ..."));
|
||||
|
||||
// TODO BODY do_after: Delay
|
||||
_disconnectedOrgans.Add(target);
|
||||
}
|
||||
|
||||
private void RemoveOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
if (_disconnectedOrgans.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_disconnectedOrgans.Count == 1)
|
||||
{
|
||||
RemoveOrganSurgeryCallback(_disconnectedOrgans[0], container, surgeon, performer);
|
||||
}
|
||||
else
|
||||
{
|
||||
surgeon.RequestMechanism(_disconnectedOrgans, RemoveOrganSurgeryCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon,
|
||||
IEntity performer)
|
||||
{
|
||||
if (Parent == null || target == null || !Parent.Mechanisms.Contains(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Remove the organ..."));
|
||||
|
||||
// TODO BODY do_after: Delay
|
||||
Parent.RemoveMechanism(target, performer.Transform.Coordinates);
|
||||
_disconnectedOrgans.Remove(target);
|
||||
}
|
||||
|
||||
private void RemoveBodyPartSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
if (!(container is IBody body)) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Saw off the limb!"));
|
||||
|
||||
// TODO BODY do_after: Delay
|
||||
body.RemovePart(Parent, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface representing an entity capable of performing surgery (performing operations on an
|
||||
/// <see cref="SurgeryDataComponent"/> class).
|
||||
/// For an example see <see cref="SurgeryToolComponent"/>, which inherits from this class.
|
||||
/// </summary>
|
||||
public interface ISurgeon
|
||||
{
|
||||
public delegate void MechanismRequestCallback(
|
||||
IMechanism target,
|
||||
IBodyPartContainer container,
|
||||
ISurgeon surgeon,
|
||||
IEntity performer);
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to perform a single surgery step (in seconds).
|
||||
/// </summary>
|
||||
public float BaseOperationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When performing a surgery, the <see cref="SurgeryDataComponent"/> may sometimes require selecting from a set of Mechanisms
|
||||
/// to operate on.
|
||||
/// This function is called in that scenario, and it is expected that you call the callback with one mechanism from the
|
||||
/// provided list.
|
||||
/// </summary>
|
||||
public void RequestMechanism(IEnumerable<IMechanism> options, MechanismRequestCallback callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
/// <summary>
|
||||
/// This data class represents the state of a <see cref="IBodyPart"/> in
|
||||
/// regards to everything surgery related - whether there's an incision on
|
||||
/// it, whether the bone is broken, etc.
|
||||
/// </summary>
|
||||
public abstract class SurgeryDataComponent : Component
|
||||
{
|
||||
protected delegate void SurgeryAction(IBodyPartContainer container, ISurgeon surgeon, IEntity performer);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IBodyPart"/> this
|
||||
/// <see cref="SurgeryDataComponent"/> is attached to.
|
||||
/// </summary>
|
||||
protected IBodyPart? Parent => Owner.GetComponentOrNull<IBodyPart>();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="BodyPartType"/> of the parent
|
||||
/// <see cref="IBodyPart"/>.
|
||||
/// </summary>
|
||||
protected BodyPartType? ParentType => Parent?.PartType;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the description of this current <see cref="IBodyPart"/> to
|
||||
/// be shown upon observing the given entity.
|
||||
/// </summary>
|
||||
public abstract string GetDescription(IEntity target);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a <see cref="IMechanism"/> can be added into the
|
||||
/// <see cref="IBodyPart"/> this <see cref="SurgeryDataComponent"/>
|
||||
/// represents.
|
||||
/// </summary>
|
||||
public abstract bool CanAddMechanism(IMechanism mechanism);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given <see cref="IBodyPart"/> can be connected
|
||||
/// to the <see cref="IBodyPart"/> this <see cref="SurgeryDataComponent"/>
|
||||
/// represents.
|
||||
/// </summary>
|
||||
public abstract bool CanAttachBodyPart(IBodyPart part);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delegate corresponding to the surgery step using the given
|
||||
/// <see cref="SurgeryType"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The corresponding surgery action or null if no step can be
|
||||
/// performed.
|
||||
/// </returns>
|
||||
protected abstract SurgeryAction? GetSurgeryStep(SurgeryType toolType);
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given <see cref="SurgeryType"/> can be used to
|
||||
/// perform a surgery on the <see cref="IBodyPart"/> this
|
||||
/// <see cref="SurgeryDataComponent"/> represents.
|
||||
/// </summary>
|
||||
public bool CheckSurgery(SurgeryType toolType)
|
||||
{
|
||||
return GetSurgeryStep(toolType) != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to perform surgery of the given <see cref="SurgeryType"/>.
|
||||
/// </summary>
|
||||
/// <param name="surgeryType">
|
||||
/// The <see cref="SurgeryType"/> used for this surgery.
|
||||
/// </param>
|
||||
/// <param name="container">
|
||||
/// The container where the surgery is being done.
|
||||
/// </param>
|
||||
/// <param name="surgeon">
|
||||
/// The entity being used to perform the surgery.
|
||||
/// </param>
|
||||
/// <param name="performer">The entity performing the surgery.</param>
|
||||
/// <returns>True if successful, false otherwise.</returns>
|
||||
public bool PerformSurgery(SurgeryType surgeryType, IBodyPartContainer container, ISurgeon surgeon,
|
||||
IEntity performer)
|
||||
{
|
||||
var step = GetSurgeryStep(surgeryType);
|
||||
|
||||
if (step == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
step(container, surgeon, performer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a surgery operation that can be performed.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum SurgeryType
|
||||
{
|
||||
None = 0,
|
||||
Incision,
|
||||
Retraction,
|
||||
Cauterization,
|
||||
VesselCompression,
|
||||
Drilling,
|
||||
Amputation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum SurgeryUIKey
|
||||
{
|
||||
Key
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects.Components.UserInterface;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public class RequestBodyPartSurgeryUIMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public Dictionary<string, int> Targets;
|
||||
|
||||
public RequestBodyPartSurgeryUIMessage(Dictionary<string, int> targets)
|
||||
{
|
||||
Targets = targets;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class RequestMechanismSurgeryUIMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public Dictionary<string, int> Targets;
|
||||
|
||||
public RequestMechanismSurgeryUIMessage(Dictionary<string, int> targets)
|
||||
{
|
||||
Targets = targets;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class RequestBodyPartSlotSurgeryUIMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public Dictionary<string, int> Targets;
|
||||
|
||||
public RequestBodyPartSlotSurgeryUIMessage(Dictionary<string, int> targets)
|
||||
{
|
||||
Targets = targets;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class ReceiveBodyPartSurgeryUIMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public int SelectedOptionId;
|
||||
|
||||
public ReceiveBodyPartSurgeryUIMessage(int selectedOptionId)
|
||||
{
|
||||
SelectedOptionId = selectedOptionId;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class ReceiveMechanismSurgeryUIMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public int SelectedOptionId;
|
||||
|
||||
public ReceiveMechanismSurgeryUIMessage(int selectedOptionId)
|
||||
{
|
||||
SelectedOptionId = selectedOptionId;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class ReceiveBodyPartSlotSurgeryUIMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public int SelectedOptionId;
|
||||
|
||||
public ReceiveBodyPartSlotSurgeryUIMessage(int selectedOptionId)
|
||||
{
|
||||
SelectedOptionId = selectedOptionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Body.Template
|
||||
{
|
||||
/// <summary>
|
||||
/// Prototype for the BodyTemplate class.
|
||||
/// </summary>
|
||||
[Prototype("bodyTemplate")]
|
||||
[Serializable, NetSerializable]
|
||||
public class BodyTemplatePrototype : IPrototype, IIndexedPrototype
|
||||
{
|
||||
private string _id;
|
||||
private string _name;
|
||||
private string _centerSlot;
|
||||
private Dictionary<string, BodyPartType> _slots;
|
||||
private Dictionary<string, List<string>> _connections;
|
||||
private Dictionary<string, string> _layers;
|
||||
private Dictionary<string, string> _mechanismLayers;
|
||||
|
||||
[ViewVariables] public string ID => _id;
|
||||
|
||||
[ViewVariables] public string Name => _name;
|
||||
|
||||
[ViewVariables] public string CenterSlot => _centerSlot;
|
||||
|
||||
[ViewVariables] public Dictionary<string, BodyPartType> Slots => new Dictionary<string, BodyPartType>(_slots);
|
||||
|
||||
[ViewVariables]
|
||||
public Dictionary<string, List<string>> Connections =>
|
||||
_connections.ToDictionary(x => x.Key, x => x.Value.ToList());
|
||||
|
||||
[ViewVariables] public Dictionary<string, string> Layers => new Dictionary<string, string>(_layers);
|
||||
|
||||
[ViewVariables] public Dictionary<string, string> MechanismLayers => new Dictionary<string, string>(_mechanismLayers);
|
||||
|
||||
public virtual void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
var serializer = YamlObjectSerializer.NewReader(mapping);
|
||||
serializer.DataField(ref _id, "id", string.Empty);
|
||||
serializer.DataField(ref _name, "name", string.Empty);
|
||||
serializer.DataField(ref _centerSlot, "centerSlot", string.Empty);
|
||||
serializer.DataField(ref _slots, "slots", new Dictionary<string, BodyPartType>());
|
||||
serializer.DataField(ref _connections, "connections", new Dictionary<string, List<string>>());
|
||||
serializer.DataField(ref _layers, "layers", new Dictionary<string, string>());
|
||||
serializer.DataField(ref _mechanismLayers, "mechanismLayers", new Dictionary<string, string>());
|
||||
|
||||
//Our prototypes don't force the user to define a BodyPart connection twice. E.g. Head: Torso v.s. Torso: Head.
|
||||
//The user only has to do one. We want it to be that way in the code, though, so this cleans that up.
|
||||
var cleanedConnections = new Dictionary<string, List<string>>();
|
||||
foreach (var targetSlotName in _slots.Keys)
|
||||
{
|
||||
var tempConnections = new List<string>();
|
||||
foreach (var (slotName, slotConnections) in _connections)
|
||||
{
|
||||
if (slotName == targetSlotName)
|
||||
{
|
||||
foreach (var connection in slotConnections)
|
||||
{
|
||||
if (!tempConnections.Contains(connection))
|
||||
{
|
||||
tempConnections.Add(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (slotConnections.Contains(targetSlotName))
|
||||
{
|
||||
tempConnections.Add(slotName);
|
||||
}
|
||||
}
|
||||
|
||||
if (tempConnections.Count > 0)
|
||||
{
|
||||
cleanedConnections.Add(targetSlotName, tempConnections);
|
||||
}
|
||||
}
|
||||
|
||||
_connections = cleanedConnections;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user