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:
DrSmugleaf
2020-10-10 15:25:13 +02:00
committed by GitHub
parent 73c730d06c
commit dd385a0511
165 changed files with 4232 additions and 4650 deletions

View File

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

View File

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

View File

@@ -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) { }
}
}

View File

@@ -0,0 +1,8 @@
#nullable enable
namespace Content.Shared.GameObjects.Components.Body.Behavior
{
public abstract class SharedHeartBehaviorComponent : MechanismBehaviorComponent
{
public override string Name => "Heart";
}
}

View File

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

View File

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

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

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

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

View File

@@ -1,8 +0,0 @@
using Content.Shared.GameObjects.Components.Damage;
namespace Content.Shared.GameObjects.Components.Body
{
public interface ISharedBodyManagerComponent : IDamageableComponent
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Body.Surgery
{
[Serializable, NetSerializable]
public enum SurgeryUIKey
{
Key
}
}

View File

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

View File

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

View File

@@ -1,40 +1,126 @@
using System;
#nullable enable
using System;
using Content.Shared.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.GameObjects.Components.Chemistry
{
public class SharedSolutionContainerComponent : Component
public abstract class SharedSolutionContainerComponent : Component
{
public override string Name => "SolutionContainer";
/// <inheritdoc />
public sealed override uint? NetID => ContentNetIDs.SOLUTION;
[Serializable, NetSerializable]
public class SolutionComponentState : ComponentState
private Solution _solution = new Solution();
private ReagentUnit _maxVolume;
private Color _substanceColor;
/// <summary>
/// The contained solution.
/// </summary>
[ViewVariables]
public Solution Solution
{
public SolutionComponentState() : base(ContentNetIDs.SOLUTION) { }
get => _solution;
set
{
if (_solution == value)
{
return;
}
_solution = value;
Dirty();
}
}
/// <summary>
/// The total volume of all the of the reagents in the container.
/// </summary>
[ViewVariables]
public ReagentUnit CurrentVolume => Solution.TotalVolume;
/// <summary>
/// The maximum volume of the container.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit MaxVolume
{
get => _maxVolume;
set
{
if (_maxVolume == value)
{
return;
}
_maxVolume = value;
Dirty();
}
}
/// <summary>
/// The current blended color of all the reagents in the container.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public virtual Color SubstanceColor
{
get => _substanceColor;
set
{
if (_substanceColor == value)
{
return;
}
_substanceColor = value;
Dirty();
}
}
/// <summary>
/// The current capabilities of this container (is the top open to pour? can I inject it into another object?).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public SolutionContainerCaps Capabilities { get; set; }
public abstract bool CanAddSolution(Solution solution);
public abstract bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false);
public abstract bool TryRemoveReagent(string reagentId, ReagentUnit quantity);
/// <inheritdoc />
public override ComponentState GetComponentState()
{
return new SolutionComponentState();
return new SolutionContainerComponentState(Solution);
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState == null)
if (!(curState is SolutionContainerComponentState state))
{
return;
}
// var compState = (SolutionComponentState)curState;
// Is there anything we even need to sync with client?
_solution = state.Solution;
}
}
[Serializable, NetSerializable]
public class SolutionContainerComponentState : ComponentState
{
public readonly Solution Solution;
public SolutionContainerComponentState(Solution solution) : base(ContentNetIDs.SOLUTION)
{
Solution = solution;
}
}
}

View File

@@ -5,7 +5,7 @@ using Content.Shared.Damage;
using Content.Shared.Damage.DamageContainer;
using Content.Shared.Damage.ResistanceSet;
using Content.Shared.Interfaces.GameObjects.Components;
using Mono.Collections.Generic;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
@@ -25,59 +25,42 @@ namespace Content.Shared.GameObjects.Components.Damage
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
// TODO define these in yaml?
public const string DefaultDamageContainer = "metallicDamageContainer";
public const string DefaultResistanceSet = "defaultResistances";
public override string Name => "Damageable";
private DamageState _currentDamageState;
private DamageState _damageState;
private DamageFlag _flags;
public event Action<HealthChangedEventArgs>? HealthChangedEvent;
/// <summary>
/// The threshold of damage, if any, above which the entity enters crit.
/// -1 means that this entity cannot go into crit.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int? CriticalThreshold { get; set; }
/// <summary>
/// The threshold of damage, if any, above which the entity dies.
/// -1 means that this entity cannot die.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int? DeadThreshold { get; set; }
[ViewVariables] private ResistanceSet Resistance { get; set; } = default!;
[ViewVariables] private ResistanceSet Resistances { get; set; } = default!;
[ViewVariables] private DamageContainer Damage { get; set; } = default!;
public Dictionary<DamageState, int> Thresholds { get; set; } = new Dictionary<DamageState, int>();
public virtual List<DamageState> SupportedDamageStates
{
get
{
var states = new List<DamageState> {DamageState.Alive};
if (CriticalThreshold != null)
{
states.Add(DamageState.Critical);
}
if (DeadThreshold != null)
{
states.Add(DamageState.Dead);
}
states.AddRange(Thresholds.Keys);
return states;
}
}
public virtual DamageState CurrentDamageState
public virtual DamageState CurrentState
{
get => _currentDamageState;
get => _damageState;
set
{
var old = _currentDamageState;
_currentDamageState = value;
var old = _damageState;
_damageState = value;
if (old != value)
{
@@ -128,17 +111,36 @@ namespace Content.Shared.GameObjects.Components.Damage
{
base.ExposeData(serializer);
// TODO DAMAGE Serialize as a dictionary of damage states to thresholds
serializer.DataReadWriteFunction(
"criticalThreshold",
-1,
t => CriticalThreshold = t == -1 ? (int?) null : t,
() => CriticalThreshold ?? -1);
null,
t =>
{
if (t == null)
{
return;
}
Thresholds[DamageState.Critical] = t.Value;
},
() => Thresholds.TryGetValue(DamageState.Critical, out var value) ? value : (int?) null);
serializer.DataReadWriteFunction(
"deadThreshold",
-1,
t => DeadThreshold = t == -1 ? (int?) null : t,
() => DeadThreshold ?? -1);
null,
t =>
{
if (t == null)
{
return;
}
Thresholds[DamageState.Dead] = t.Value;
},
() => Thresholds.TryGetValue(DamageState.Dead, out var value) ? value : (int?) null);
serializer.DataField(ref _damageState, "damageState", DamageState.Alive);
serializer.DataReadWriteFunction(
"flags",
@@ -172,32 +174,26 @@ namespace Content.Shared.GameObjects.Components.Damage
return writeFlags;
});
if (serializer.Reading)
{
// Doesn't write to file, TODO?
// Yes, TODO
var containerId = "biologicalDamageContainer";
var resistanceId = "defaultResistances";
serializer.DataField(ref containerId, "damageContainer", "biologicalDamageContainer");
serializer.DataField(ref resistanceId, "resistances", "defaultResistances");
if (!_prototypeManager.TryIndex(containerId!, out DamageContainerPrototype damage))
// TODO DAMAGE Serialize damage done and resistance changes
serializer.DataReadWriteFunction(
"damagePrototype",
DefaultDamageContainer,
prototype =>
{
throw new InvalidOperationException(
$"No {nameof(DamageContainerPrototype)} found with name {containerId}");
}
var damagePrototype = _prototypeManager.Index<DamageContainerPrototype>(prototype);
Damage = new DamageContainer(OnHealthChanged, damagePrototype);
},
() => Damage.ID);
Damage = new DamageContainer(OnHealthChanged, damage);
if (!_prototypeManager.TryIndex(resistanceId!, out ResistanceSetPrototype resistance))
serializer.DataReadWriteFunction(
"resistancePrototype",
DefaultResistanceSet,
prototype =>
{
throw new InvalidOperationException(
$"No {nameof(ResistanceSetPrototype)} found with name {resistanceId}");
}
Resistance = new ResistanceSet(resistance);
}
var resistancePrototype = _prototypeManager.Index<ResistanceSetPrototype>(prototype);
Resistances = new ResistanceSet(resistancePrototype);
},
() => Resistances.ID);
}
public override void Initialize()
@@ -210,6 +206,13 @@ namespace Content.Shared.GameObjects.Components.Damage
}
}
protected override void Startup()
{
base.Startup();
ForceHealthChangedEvent();
}
public bool TryGetDamage(DamageType type, out int damage)
{
return Damage.TryGetDamageValue(type, out damage);
@@ -229,7 +232,7 @@ namespace Content.Shared.GameObjects.Components.Damage
var finalDamage = amount;
if (!ignoreResistances)
{
finalDamage = Resistance.CalculateDamage(type, amount);
finalDamage = Resistances.CalculateDamage(type, amount);
}
Damage.ChangeDamageValue(type, finalDamage);
@@ -362,6 +365,32 @@ namespace Content.Shared.GameObjects.Components.Damage
OnHealthChanged(data);
}
public (int current, int max)? Health(DamageState threshold)
{
if (!SupportedDamageStates.Contains(threshold) ||
!Thresholds.TryGetValue(threshold, out var thresholdValue))
{
return null;
}
var current = thresholdValue - TotalDamage;
return (current, thresholdValue);
}
public bool TryHealth(DamageState threshold, out (int current, int max) health)
{
var temp = Health(threshold);
if (temp == null)
{
health = (default, default);
return false;
}
health = temp.Value;
return true;
}
private void OnHealthChanged(List<HealthChangeData> changes)
{
var args = new HealthChangedEventArgs(this, changes);
@@ -372,19 +401,21 @@ namespace Content.Shared.GameObjects.Components.Damage
protected virtual void OnHealthChanged(HealthChangedEventArgs e)
{
if (CurrentDamageState != DamageState.Dead)
if (CurrentState != DamageState.Dead)
{
if (DeadThreshold != -1 && TotalDamage > DeadThreshold)
if (Thresholds.TryGetValue(DamageState.Dead, out var deadThreshold) &&
TotalDamage > deadThreshold)
{
CurrentDamageState = DamageState.Dead;
CurrentState = DamageState.Dead;
}
else if (CriticalThreshold != -1 && TotalDamage > CriticalThreshold)
else if (Thresholds.TryGetValue(DamageState.Critical, out var critThreshold) &&
TotalDamage > critThreshold)
{
CurrentDamageState = DamageState.Critical;
CurrentState = DamageState.Critical;
}
else
{
CurrentDamageState = DamageState.Alive;
CurrentState = DamageState.Alive;
}
}
@@ -400,5 +431,19 @@ namespace Content.Shared.GameObjects.Components.Damage
ChangeDamage(DamageType.Radiation, totalDamage, false, radiation.Owner);
}
public void OnExplosion(ExplosionEventArgs eventArgs)
{
var damage = eventArgs.Severity switch
{
ExplosionSeverity.Light => 20,
ExplosionSeverity.Heavy => 60,
ExplosionSeverity.Destruction => 250,
_ => throw new ArgumentOutOfRangeException()
};
ChangeDamage(DamageType.Piercing, damage, false);
ChangeDamage(DamageType.Heat, damage, false);
}
}
}

View File

@@ -19,16 +19,18 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </summary>
event Action<HealthChangedEventArgs> HealthChangedEvent;
Dictionary<DamageState, int> Thresholds { get; }
/// <summary>
/// List of all <see cref="DamageState">DamageStates</see> that
/// <see cref="CurrentDamageState"/> can be.
/// List of all <see cref="Damage.DamageState">DamageStates</see> that
/// <see cref="CurrentState"/> can be.
/// </summary>
List<DamageState> SupportedDamageStates { get; }
/// <summary>
/// The <see cref="DamageState"/> currently representing this component.
/// The <see cref="Damage.DamageState"/> currently representing this component.
/// </summary>
DamageState CurrentDamageState { get; }
DamageState CurrentState { get; set; }
/// <summary>
/// Sum of all damages taken.
@@ -157,26 +159,37 @@ namespace Content.Shared.GameObjects.Components.Damage
/// </summary>
void ForceHealthChangedEvent();
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
{
var damage = eventArgs.Severity switch
{
ExplosionSeverity.Light => 20,
ExplosionSeverity.Heavy => 60,
ExplosionSeverity.Destruction => 250,
_ => throw new ArgumentOutOfRangeException()
};
/// <summary>
/// Calculates the health of an entity until it enters
/// <see cref="threshold"/>.
/// </summary>
/// <param name="threshold">The state to use as a threshold.</param>
/// <returns>
/// The current and maximum health on this entity based on
/// <see cref="threshold"/>, or null if the state is not supported.
/// </returns>
(int current, int max)? Health(DamageState threshold);
ChangeDamage(DamageType.Piercing, damage, false);
ChangeDamage(DamageType.Heat, damage, false);
}
/// <summary>
/// Calculates the health of an entity until it enters
/// <see cref="threshold"/>.
/// </summary>
/// <param name="threshold">The state to use as a threshold.</param>
/// <param name="health">
/// The current and maximum health on this entity based on
/// <see cref="threshold"/>, or null if the state is not supported.
/// </param>
/// <returns>
/// True if <see cref="threshold"/> is supported, false otherwise.
/// </returns>
bool TryHealth(DamageState threshold, [NotNullWhen(true)] out (int current, int max) health);
}
/// <summary>
/// Data class with information on how to damage a
/// <see cref="IDamageableComponent"/>.
/// While not necessary to damage for all instances, classes such as
/// <see cref="SharedBodyManagerComponent"/> may require it for extra data
/// <see cref="SharedBodyComponent"/> may require it for extra data
/// (such as selecting which limb to target).
/// </summary>
public class HealthChangeParams : EventArgs

View File

@@ -97,9 +97,9 @@ namespace Content.Shared.GameObjects.Components.Mobs.State
public void OnHealthChanged(HealthChangedEventArgs e)
{
if (e.Damageable.CurrentDamageState != CurrentDamageState)
if (e.Damageable.CurrentState != CurrentDamageState)
{
CurrentDamageState = e.Damageable.CurrentDamageState;
CurrentDamageState = e.Damageable.CurrentState;
CurrentMobState.ExitState(Owner);
CurrentMobState = Behavior[CurrentDamageState];
CurrentMobState.EnterState(Owner);

View File

@@ -270,7 +270,7 @@ namespace Content.Shared.GameObjects.Components.Movement
bool ICollideSpecial.PreventCollide(IPhysBody collidedWith)
{
// Don't collide with other mobs
return collidedWith.Entity.HasComponent<ISharedBodyManagerComponent>();
return collidedWith.Entity.HasComponent<IBody>();
}
[Serializable, NetSerializable]