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,33 @@
|
||||
using Content.Shared.GameObjects.Components.Body.Behavior;
|
||||
using Content.Shared.GameObjects.Components.Body.Networks;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedHeartBehaviorComponent))]
|
||||
public class HeartBehaviorComponent : SharedHeartBehaviorComponent
|
||||
{
|
||||
private float _accumulatedFrameTime;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
// TODO BODY do between pre and metabolism
|
||||
if (Mechanism?.Body == null ||
|
||||
!Mechanism.Body.Owner.HasComponent<SharedBloodstreamComponent>())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update at most once per second
|
||||
_accumulatedFrameTime += frameTime;
|
||||
|
||||
// TODO: Move/accept/process bloodstream reagents only when the heart is pumping
|
||||
if (_accumulatedFrameTime >= 1)
|
||||
{
|
||||
// bloodstream.Update(_accumulatedFrameTime);
|
||||
_accumulatedFrameTime -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,28 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.GameObjects.Components.Body.Behavior;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class LungComponent : Component, IGasMixtureHolder
|
||||
[ComponentReference(typeof(SharedLungBehaviorComponent))]
|
||||
public class LungBehaviorComponent : SharedLungBehaviorComponent
|
||||
{
|
||||
public override string Name => "Lung";
|
||||
|
||||
private float _accumulatedFrameTime;
|
||||
|
||||
[ViewVariables] public GasMixture Air { get; set; }
|
||||
[ViewVariables] public GasMixture Air { get; set; } = default!;
|
||||
|
||||
[ViewVariables] public LungStatus Status { get; set; }
|
||||
[ViewVariables] public override float Temperature => Air.Temperature;
|
||||
|
||||
[ViewVariables] public float CycleDelay { get; set; }
|
||||
[ViewVariables] public override float Volume => Air.Volume;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
@@ -42,11 +41,52 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
Atmospherics.NormalBodyTemperature,
|
||||
temp => Air.Temperature = temp,
|
||||
() => Air.Temperature);
|
||||
|
||||
serializer.DataField(this, l => l.CycleDelay, "cycleDelay", 2);
|
||||
}
|
||||
|
||||
public void Update(float frameTime)
|
||||
public override void Gasp()
|
||||
{
|
||||
Owner.PopupMessageEveryone("Gasp");
|
||||
Inhale(CycleDelay);
|
||||
}
|
||||
|
||||
public void Transfer(GasMixture from, GasMixture to, float ratio)
|
||||
{
|
||||
var removed = from.RemoveRatio(ratio);
|
||||
var toOld = to.Gases.ToArray();
|
||||
|
||||
to.Merge(removed);
|
||||
|
||||
for (var gas = 0; gas < Atmospherics.TotalNumberOfGases; gas++)
|
||||
{
|
||||
var newAmount = to.GetMoles(gas);
|
||||
var oldAmount = toOld[gas];
|
||||
var delta = newAmount - oldAmount;
|
||||
|
||||
removed.AdjustMoles(gas, -delta);
|
||||
}
|
||||
|
||||
from.Merge(removed);
|
||||
}
|
||||
|
||||
public void ToBloodstream(GasMixture mixture)
|
||||
{
|
||||
if (Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var to = bloodstream.Air;
|
||||
|
||||
to.Merge(mixture);
|
||||
mixture.Clear();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (Status == LungStatus.None)
|
||||
{
|
||||
@@ -85,39 +125,7 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
_accumulatedFrameTime = absoluteTime - delay;
|
||||
}
|
||||
|
||||
public void Transfer(GasMixture from, GasMixture to, float ratio)
|
||||
{
|
||||
var removed = from.RemoveRatio(ratio);
|
||||
var toOld = to.Gases.ToArray();
|
||||
|
||||
to.Merge(removed);
|
||||
|
||||
for (var gas = 0; gas < Atmospherics.TotalNumberOfGases; gas++)
|
||||
{
|
||||
var newAmount = to.GetMoles(gas);
|
||||
var oldAmount = toOld[gas];
|
||||
var delta = newAmount - oldAmount;
|
||||
|
||||
removed.AdjustMoles(gas, -delta);
|
||||
}
|
||||
|
||||
from.Merge(removed);
|
||||
}
|
||||
|
||||
public void ToBloodstream(GasMixture mixture)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var to = bloodstream.Air;
|
||||
|
||||
to.Merge(mixture);
|
||||
mixture.Clear();
|
||||
}
|
||||
|
||||
public void Inhale(float frameTime)
|
||||
public override void Inhale(float frameTime)
|
||||
{
|
||||
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
|
||||
{
|
||||
@@ -135,7 +143,7 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
ToBloodstream(Air);
|
||||
}
|
||||
|
||||
public void Exhale(float frameTime)
|
||||
public override void Exhale(float frameTime)
|
||||
{
|
||||
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
|
||||
{
|
||||
@@ -148,7 +156,12 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
public void Exhale(float frameTime, GasMixture to)
|
||||
{
|
||||
// TODO: Make the bloodstream separately pump toxins into the lungs, making the lungs' only job to empty.
|
||||
if (!Owner.TryGetComponent(out BloodstreamComponent bloodstream))
|
||||
if (Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -171,18 +184,5 @@ namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
|
||||
Air.Merge(lungRemoved);
|
||||
}
|
||||
|
||||
public void Gasp()
|
||||
{
|
||||
Owner.PopupMessageEveryone("Gasp");
|
||||
Inhale(CycleDelay);
|
||||
}
|
||||
}
|
||||
|
||||
public enum LungStatus
|
||||
{
|
||||
None = 0,
|
||||
Inhaling,
|
||||
Exhaling
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body.Behavior;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedStomachBehaviorComponent))]
|
||||
public class StomachBehaviorComponent : SharedStomachBehaviorComponent
|
||||
{
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
if (!Owner.EnsureComponent(out SolutionContainerComponent solution))
|
||||
{
|
||||
Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
|
||||
}
|
||||
|
||||
solution.MaxVolume = InitialMaxVolume;
|
||||
}
|
||||
}
|
||||
}
|
||||
329
Content.Server/GameObjects/Components/Body/BodyCommands.cs
Normal file
329
Content.Server/GameObjects/Components/Body/BodyCommands.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Robust.Server.Interfaces.Console;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
class AddHandCommand : IClientCommand
|
||||
{
|
||||
public const string DefaultHandPrototype = "LeftHandHuman";
|
||||
|
||||
public string Command => "addhand";
|
||||
public string Description => "Adds a hand to your entity.";
|
||||
public string Help => $"Usage: {Command} <entityUid> <handPrototypeId> / {Command} <entityUid> / {Command} <handPrototypeId> / {Command}";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
if (args.Length > 1)
|
||||
{
|
||||
shell.SendText(player, Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
|
||||
IEntity entity;
|
||||
IEntity hand;
|
||||
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
shell.SendText(player, "Only a player can run this command without arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.AttachedEntity == null)
|
||||
{
|
||||
shell.SendText(player, "You don't have an entity to add a hand to.");
|
||||
return;
|
||||
}
|
||||
|
||||
entity = player.AttachedEntity;
|
||||
hand = entityManager.SpawnEntity(DefaultHandPrototype, entity.Transform.Coordinates);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
if (EntityUid.TryParse(args[0], out var uid))
|
||||
{
|
||||
if (!entityManager.TryGetEntity(uid, out var parsedEntity))
|
||||
{
|
||||
shell.SendText(player, $"No entity found with uid {uid}");
|
||||
return;
|
||||
}
|
||||
|
||||
entity = parsedEntity;
|
||||
hand = entityManager.SpawnEntity(DefaultHandPrototype, entity.Transform.Coordinates);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
shell.SendText(player,
|
||||
"You must specify an entity to add a hand to when using this command from the server terminal.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.AttachedEntity == null)
|
||||
{
|
||||
shell.SendText(player, "You don't have an entity to add a hand to.");
|
||||
return;
|
||||
}
|
||||
|
||||
entity = player.AttachedEntity;
|
||||
hand = entityManager.SpawnEntity(args[0], entity.Transform.Coordinates);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
if (!EntityUid.TryParse(args[0], out var uid))
|
||||
{
|
||||
shell.SendText(player, $"{args[0]} is not a valid entity uid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entityManager.TryGetEntity(uid, out var parsedEntity))
|
||||
{
|
||||
shell.SendText(player, $"No entity exists with uid {uid}.");
|
||||
return;
|
||||
}
|
||||
|
||||
entity = parsedEntity;
|
||||
|
||||
if (!prototypeManager.HasIndex<EntityPrototype>(args[1]))
|
||||
{
|
||||
shell.SendText(player, $"No hand entity exists with id {args[1]}.");
|
||||
return;
|
||||
}
|
||||
|
||||
hand = entityManager.SpawnEntity(args[1], entity.Transform.Coordinates);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
shell.SendText(player, Help);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entity.TryGetComponent(out IBody? body))
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
|
||||
|
||||
shell.SendText(player, text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hand.TryGetComponent(out IBodyPart? part))
|
||||
{
|
||||
shell.SendText(player, $"Hand entity {hand} does not have a {nameof(IBodyPart)} component.");
|
||||
return;
|
||||
}
|
||||
|
||||
var slot = part.GetHashCode().ToString();
|
||||
var response = body.TryAddPart(slot, part, true)
|
||||
? $"Added hand to entity {entity.Name}"
|
||||
: $"Error occurred trying to add a hand to entity {entity.Name}";
|
||||
|
||||
shell.SendText(player, response);
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveHandCommand : IClientCommand
|
||||
{
|
||||
public string Command => "removehand";
|
||||
public string Description => "Removes a hand from your entity.";
|
||||
public string Help => $"Usage: {Command}";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
shell.SendText(player, "Only a player can run this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.AttachedEntity == null)
|
||||
{
|
||||
shell.SendText(player, "You have no entity.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.AttachedEntity.TryGetBody(out var body))
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
|
||||
|
||||
shell.SendText(player, text);
|
||||
return;
|
||||
}
|
||||
|
||||
var hand = body.Parts.FirstOrDefault(x => x.Value.PartType == BodyPartType.Hand);
|
||||
if (hand.Value.Equals(default))
|
||||
{
|
||||
shell.SendText(player, "You have no hands.");
|
||||
}
|
||||
else
|
||||
{
|
||||
body.RemovePart(hand.Value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DestroyMechanismCommand : IClientCommand
|
||||
{
|
||||
public string Command => "destroymechanism";
|
||||
public string Description => "Destroys a mechanism from your entity";
|
||||
public string Help => $"Usage: {Command} <mechanism>";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
shell.SendText(player, "Only a player can run this command.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.SendText(player, Help);
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.AttachedEntity == null)
|
||||
{
|
||||
shell.SendText(player, "You have no entity.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.AttachedEntity.TryGetBody(out var body))
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
|
||||
|
||||
shell.SendText(player, text);
|
||||
return;
|
||||
}
|
||||
|
||||
var mechanismName = string.Join(" ", args).ToLowerInvariant();
|
||||
|
||||
foreach (var part in body.Parts.Values)
|
||||
foreach (var mechanism in part.Mechanisms)
|
||||
{
|
||||
if (mechanism.Name.ToLowerInvariant() == mechanismName)
|
||||
{
|
||||
part.DeleteMechanism(mechanism);
|
||||
shell.SendText(player, $"Mechanism with name {mechanismName} has been destroyed.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
shell.SendText(player, $"No mechanism was found with name {mechanismName}.");
|
||||
}
|
||||
}
|
||||
|
||||
class HurtCommand : IClientCommand
|
||||
{
|
||||
public string Command => "hurt";
|
||||
public string Description => "Ouch";
|
||||
public string Help => $"Usage: {Command} <type> <amount> (<entity uid/_>) (<ignoreResistance>)";
|
||||
|
||||
private void SendDamageTypes(IConsoleShell shell, IPlayerSession? player)
|
||||
{
|
||||
var msg = "";
|
||||
foreach (var dClass in Enum.GetNames(typeof(DamageClass)))
|
||||
{
|
||||
msg += $"\n{dClass}";
|
||||
var types = Enum.Parse<DamageClass>(dClass).ToTypes();
|
||||
foreach (var dType in types)
|
||||
{
|
||||
msg += $"\n - {dType}";
|
||||
}
|
||||
}
|
||||
shell.SendText(player, $"Damage Types:{msg}");
|
||||
}
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
// Check if we have enough for the dmg types to show
|
||||
if (args.Length > 0 && args[0] == "?")
|
||||
{
|
||||
SendDamageTypes(shell, player);
|
||||
return;
|
||||
}
|
||||
|
||||
// Not enough args
|
||||
if (args.Length < 2)
|
||||
{
|
||||
shell.SendText(player, Help);
|
||||
return;
|
||||
}
|
||||
|
||||
var ignoreResistance = false;
|
||||
var entityUid = player != null && player.AttachedEntityUid.HasValue ? player.AttachedEntityUid.Value : EntityUid.Invalid;
|
||||
if (!int.TryParse(args[1], out var amount) ||
|
||||
args.Length >= 3 && args[2] != "_" && !EntityUid.TryParse(args[2], out entityUid) ||
|
||||
args.Length >= 4 && !bool.TryParse(args[3], out ignoreResistance))
|
||||
{
|
||||
shell.SendText(player, Help);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entityUid == EntityUid.Invalid)
|
||||
{
|
||||
shell.SendText(player, "Not a valid entity.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetEntity(entityUid, out var ent))
|
||||
{
|
||||
shell.SendText(player, "Entity couldn't be found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ent.TryGetComponent(out IDamageableComponent? damageable))
|
||||
{
|
||||
shell.SendText(player, "Entity can't be damaged.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Enum.TryParse<DamageClass>(args[0], true, out var dmgClass))
|
||||
{
|
||||
if (!damageable.ChangeDamage(dmgClass, amount, ignoreResistance))
|
||||
shell.SendText(player, "Something went wrong!");
|
||||
return;
|
||||
}
|
||||
// Fall back to DamageType
|
||||
else if (Enum.TryParse<DamageType>(args[0], true, out var dmgType))
|
||||
{
|
||||
if (!damageable.ChangeDamage(dmgType, amount, ignoreResistance))
|
||||
shell.SendText(player, "Something went wrong!");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
SendDamageTypes(shell, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Content.Server/GameObjects/Components/Body/BodyComponent.cs
Normal file
85
Content.Server/GameObjects/Components/Body/BodyComponent.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
#nullable enable
|
||||
using Content.Server.Observer;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedBodyComponent))]
|
||||
[ComponentReference(typeof(IBody))]
|
||||
public class BodyComponent : SharedBodyComponent, IRelayMoveInput
|
||||
{
|
||||
private Container _container = default!;
|
||||
|
||||
protected override bool CanAddPart(string slot, IBodyPart part)
|
||||
{
|
||||
return base.CanAddPart(slot, part) && _container.CanInsert(part.Owner);
|
||||
}
|
||||
|
||||
protected override void OnAddPart(string slot, IBodyPart part)
|
||||
{
|
||||
base.OnAddPart(slot, part);
|
||||
|
||||
_container.Insert(part.Owner);
|
||||
}
|
||||
|
||||
protected override void OnRemovePart(string slot, IBodyPart part)
|
||||
{
|
||||
base.OnRemovePart(slot, part);
|
||||
|
||||
_container.ForceRemove(part.Owner);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_container = ContainerManagerComponent.Ensure<Container>($"{Name}-{nameof(BodyComponent)}", Owner);
|
||||
|
||||
foreach (var (slot, partId) in PartIds)
|
||||
{
|
||||
// Using MapPosition instead of Coordinates here prevents
|
||||
// a crash within the character preview menu in the lobby
|
||||
var entity = Owner.EntityManager.SpawnEntity(partId, Owner.Transform.MapPosition);
|
||||
|
||||
if (!entity.TryGetComponent(out IBodyPart? part))
|
||||
{
|
||||
Logger.Error($"Entity {partId} does not have a {nameof(IBodyPart)} component.");
|
||||
continue;
|
||||
}
|
||||
|
||||
TryAddPart(slot, part, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
// This is ran in Startup as entities spawned in Initialize
|
||||
// are not synced to the client since they are assumed to be
|
||||
// identical on it
|
||||
foreach (var part in Parts.Values)
|
||||
{
|
||||
part.Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void IRelayMoveInput.MoveInputPressed(ICommonSession session)
|
||||
{
|
||||
if (Owner.TryGetComponent(out IDamageableComponent? damageable) &&
|
||||
damageable.CurrentState == DamageState.Dead)
|
||||
{
|
||||
new Ghost().Execute(null, (IPlayerSession) session, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,543 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Body;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Interaction;
|
||||
using Content.Shared.Body.Part.Properties.Movement;
|
||||
using Content.Shared.Body.Part.Properties.Other;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
public partial class BodyManagerComponent
|
||||
{
|
||||
private readonly Dictionary<string, IBodyPart> _parts = new Dictionary<string, IBodyPart>();
|
||||
|
||||
[ViewVariables] public BodyPreset Preset { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// All <see cref="IBodyPart"></see> with <see cref="LegProperty"></see>
|
||||
/// that are currently affecting move speed, mapped to how big that leg
|
||||
/// they're on is.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<IBodyPart, float> _activeLegs = new Dictionary<IBodyPart, float>();
|
||||
|
||||
/// <summary>
|
||||
/// Maps <see cref="BodyTemplate"/> slot name to the <see cref="IBodyPart"/>
|
||||
/// object filling it (if there is one).
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public IReadOnlyDictionary<string, IBodyPart> Parts => _parts;
|
||||
|
||||
/// <summary>
|
||||
/// List of all occupied slots in this body, taken from the values of
|
||||
/// <see cref="Parts"/>.
|
||||
/// </summary>
|
||||
public IEnumerable<string> OccupiedSlots => Parts.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// List of all slots in this body, taken from the keys of
|
||||
/// <see cref="Template"/> slots.
|
||||
/// </summary>
|
||||
public IEnumerable<string> AllSlots => Template.Slots.Keys;
|
||||
|
||||
public bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
if (!TryAddPart(slot, part.ContainedBodyPart, force))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
part.Owner.Delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryAddPart(string slot, IBodyPart part, bool force = false)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
DebugTools.AssertNotNull(slot);
|
||||
|
||||
// Make sure the given slot exists
|
||||
if (!force)
|
||||
{
|
||||
if (!HasSlot(slot))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// And that nothing is in it
|
||||
if (!_parts.TryAdd(slot, part))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_parts[slot] = part;
|
||||
}
|
||||
|
||||
part.Body = this;
|
||||
|
||||
var argsAdded = new BodyPartAddedEventArgs(part, slot);
|
||||
|
||||
foreach (var component in Owner.GetAllComponents<IBodyPartAdded>().ToArray())
|
||||
{
|
||||
component.BodyPartAdded(argsAdded);
|
||||
}
|
||||
|
||||
// TODO: Sort this duplicate out
|
||||
OnBodyChanged();
|
||||
|
||||
if (!Template.Layers.TryGetValue(slot, out var partMap) ||
|
||||
!_reflectionManager.TryParseEnumReference(partMap, out var partEnum))
|
||||
{
|
||||
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {partMap} for body part {part.Name}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
part.RSIMap = partEnum;
|
||||
|
||||
var partMessage = new BodyPartAddedMessage(part.RSIPath, part.RSIState, partEnum);
|
||||
|
||||
SendNetworkMessage(partMessage);
|
||||
|
||||
foreach (var mechanism in part.Mechanisms)
|
||||
{
|
||||
if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
|
||||
{
|
||||
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var mechanismMessage = new MechanismSpriteAddedMessage(mechanismEnum);
|
||||
|
||||
SendNetworkMessage(mechanismMessage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HasPart(string 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);
|
||||
}
|
||||
|
||||
public bool RemovePart(string slot, bool drop)
|
||||
{
|
||||
DebugTools.AssertNotNull(slot);
|
||||
|
||||
if (!_parts.Remove(slot, out var part))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IEntity? dropped = null;
|
||||
if (drop)
|
||||
{
|
||||
part.SpawnDropped(out dropped);
|
||||
}
|
||||
|
||||
part.Body = null;
|
||||
|
||||
var args = new BodyPartRemovedEventArgs(part, slot);
|
||||
|
||||
foreach (var component in Owner.GetAllComponents<IBodyPartRemoved>())
|
||||
{
|
||||
component.BodyPartRemoved(args);
|
||||
}
|
||||
|
||||
if (part.RSIMap != null)
|
||||
{
|
||||
var message = new BodyPartRemovedMessage(part.RSIMap, dropped?.Uid);
|
||||
SendNetworkMessage(message);
|
||||
}
|
||||
|
||||
foreach (var mechanism in part.Mechanisms)
|
||||
{
|
||||
if (!Template.MechanismLayers.TryGetValue(mechanism.Id, out var mechanismMap))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_reflectionManager.TryParseEnumReference(mechanismMap, out var mechanismEnum))
|
||||
{
|
||||
Logger.Warning($"Template {Template.Name} has an invalid RSI map key {mechanismMap} for mechanism {mechanism.Id}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var mechanismMessage = new MechanismSpriteRemovedMessage(mechanismEnum);
|
||||
|
||||
SendNetworkMessage(mechanismMessage);
|
||||
}
|
||||
|
||||
if (CurrentDamageState == DamageState.Dead) return true;
|
||||
|
||||
// creadth: fall down if no legs
|
||||
if (part.PartType == BodyPartType.Leg && Parts.Count(x => x.Value.PartType == BodyPartType.Leg) == 0)
|
||||
{
|
||||
EntitySystem.Get<StandingStateSystem>().Down(Owner);
|
||||
}
|
||||
|
||||
// creadth: immediately kill entity if last vital part removed
|
||||
if (part.IsVital && Parts.Count(x => x.Value.PartType == part.PartType) == 0)
|
||||
{
|
||||
CurrentDamageState = DamageState.Dead;
|
||||
ForceHealthChangedEvent();
|
||||
}
|
||||
|
||||
if (TryGetSlotConnections(slot, out var connections))
|
||||
{
|
||||
foreach (var connectionName in connections)
|
||||
{
|
||||
if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result))
|
||||
{
|
||||
RemovePart(connectionName, drop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnBodyChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemovePart(IBodyPart part, [NotNullWhen(true)] out string? slot)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
var pair = _parts.FirstOrDefault(kvPair => kvPair.Value == part);
|
||||
|
||||
if (pair.Equals(default))
|
||||
{
|
||||
slot = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
slot = pair.Key;
|
||||
|
||||
return RemovePart(slot, false);
|
||||
}
|
||||
|
||||
public IEntity? DropPart(IBodyPart part)
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
if (!_parts.ContainsValue(part))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!RemovePart(part, out var slotName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Call disconnect on all limbs that were hanging off this limb.
|
||||
if (TryGetSlotConnections(slotName, out var connections))
|
||||
{
|
||||
// This loop is an unoptimized travesty. TODO: optimize to be less shit
|
||||
foreach (var connectionName in connections)
|
||||
{
|
||||
if (TryGetPart(connectionName, out var result) && !ConnectedToCenter(result))
|
||||
{
|
||||
RemovePart(connectionName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
part.SpawnDropped(out var dropped);
|
||||
|
||||
OnBodyChanged();
|
||||
return dropped;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
Parts.TryGetValue(Template.CenterSlot, out var center);
|
||||
return center;
|
||||
}
|
||||
|
||||
public bool HasSlot(string slot)
|
||||
{
|
||||
return Template.HasSlot(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 Template.Slots.TryGetValue(slot, out result);
|
||||
}
|
||||
|
||||
public bool TryGetSlotConnections(string slot, [NotNullWhen(true)] out List<string>? connections)
|
||||
{
|
||||
return Template.Connections.TryGetValue(slot, out connections);
|
||||
}
|
||||
|
||||
public bool TryGetPartConnections(string slot, [NotNullWhen(true)] out List<IBodyPart>? result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
if (!Template.Connections.TryGetValue(slot, out var connections))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var toReturn = new List<IBodyPart>();
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
if (TryGetPart(connection, out var partResult))
|
||||
{
|
||||
toReturn.Add(partResult);
|
||||
}
|
||||
}
|
||||
|
||||
if (toReturn.Count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = toReturn;
|
||||
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 toReturn = new List<IBodyPart>();
|
||||
|
||||
foreach (var part in Parts.Values)
|
||||
{
|
||||
if (part.PartType == type)
|
||||
{
|
||||
toReturn.Add(part);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
private void CalculateSpeed()
|
||||
{
|
||||
if (!Owner.TryGetComponent(out MovementSpeedModifierComponent? playerMover))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float speedSum = 0;
|
||||
foreach (var part in _activeLegs.Keys)
|
||||
{
|
||||
if (!part.HasProperty<LegProperty>())
|
||||
{
|
||||
_activeLegs.Remove(part);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, value) in _activeLegs)
|
||||
{
|
||||
if (key.TryGetProperty(out LegProperty? leg))
|
||||
{
|
||||
// Speed of a leg = base speed * (1+log1024(leg length))
|
||||
speedSum += leg.Speed * (1 + (float) Math.Log(value, 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
if (speedSum <= 0.001f || _activeLegs.Count <= 0)
|
||||
{
|
||||
playerMover.BaseWalkSpeed = 0.8f;
|
||||
playerMover.BaseSprintSpeed = 2.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extra legs stack diminishingly.
|
||||
// Final speed = speed sum/(leg count-log4(leg count))
|
||||
playerMover.BaseWalkSpeed =
|
||||
speedSum / (_activeLegs.Count - (float) Math.Log(_activeLegs.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>())
|
||||
{
|
||||
_activeLegs.Clear();
|
||||
var legParts = Parts.Values.Where(x => x.HasProperty(typeof(LegProperty)));
|
||||
|
||||
foreach (var part in legParts)
|
||||
{
|
||||
var footDistance = DistanceToNearestFoot(part);
|
||||
|
||||
if (Math.Abs(footDistance - float.MinValue) > 0.001f)
|
||||
{
|
||||
_activeLegs.Add(part, footDistance);
|
||||
}
|
||||
}
|
||||
|
||||
CalculateSpeed();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combined length of the distance to the nearest <see cref="BodyPart"/> with a
|
||||
/// <see cref="FootProperty"/>. Returns <see cref="float.MinValue"/>
|
||||
/// if there is no foot found. If you consider a <see cref="BodyManagerComponent"/> a node map, then it will look for
|
||||
/// a foot node from the given node. It can
|
||||
/// only search through BodyParts with <see cref="ExtensionProperty"/>.
|
||||
/// </summary>
|
||||
public float DistanceToNearestFoot(IBodyPart source)
|
||||
{
|
||||
if (source.HasProperty<FootProperty>() && source.TryGetProperty<ExtensionProperty>(out var property))
|
||||
{
|
||||
return property.ReachDistance;
|
||||
}
|
||||
|
||||
return LookForFootRecursion(source, new List<BodyPart>());
|
||||
}
|
||||
|
||||
private float LookForFootRecursion(IBodyPart current,
|
||||
ICollection<BodyPart> searchedParts)
|
||||
{
|
||||
if (!current.TryGetProperty<ExtensionProperty>(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 (!searchedParts.Contains(connection) && connection.HasProperty<FootProperty>())
|
||||
{
|
||||
return extProperty.ReachDistance;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.ReachDistance;
|
||||
}
|
||||
|
||||
return float.MinValue;
|
||||
|
||||
// No extension property, no go.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Body;
|
||||
using Content.Server.Body.Network;
|
||||
using Content.Server.GameObjects.Components.Metabolism;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Observer;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.Body.Preset;
|
||||
using Content.Shared.Body.Template;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.Reflection;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
/// <summary>
|
||||
/// Component representing a collection of <see cref="IBodyPart"></see>
|
||||
/// attached to each other.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IDamageableComponent))]
|
||||
[ComponentReference(typeof(ISharedBodyManagerComponent))]
|
||||
[ComponentReference(typeof(IBodyPartManager))]
|
||||
[ComponentReference(typeof(IBodyManagerComponent))]
|
||||
public partial class BodyManagerComponent : SharedBodyManagerComponent, IBodyPartContainer, IRelayMoveInput, IBodyManagerComponent
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IBodyNetworkFactory _bodyNetworkFactory = default!;
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
|
||||
[ViewVariables] private string _presetName = default!;
|
||||
|
||||
[ViewVariables] private readonly Dictionary<Type, BodyNetwork> _networks = new Dictionary<Type, BodyNetwork>();
|
||||
|
||||
[ViewVariables] public BodyTemplate Template { get; private set; } = default!;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataReadWriteFunction(
|
||||
"baseTemplate",
|
||||
"bodyTemplate.Humanoid",
|
||||
template =>
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(template, out BodyTemplatePrototype prototype))
|
||||
{
|
||||
// Invalid prototype
|
||||
throw new InvalidOperationException(
|
||||
$"No {nameof(BodyTemplatePrototype)} found with name {template}");
|
||||
}
|
||||
|
||||
Template = new BodyTemplate();
|
||||
Template.Initialize(prototype);
|
||||
},
|
||||
() => Template.Name);
|
||||
|
||||
serializer.DataReadWriteFunction(
|
||||
"basePreset",
|
||||
"bodyPreset.BasicHuman",
|
||||
preset =>
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(preset, out BodyPresetPrototype prototype))
|
||||
{
|
||||
// Invalid prototype
|
||||
throw new InvalidOperationException(
|
||||
$"No {nameof(BodyPresetPrototype)} found with name {preset}");
|
||||
}
|
||||
|
||||
Preset = new BodyPreset();
|
||||
Preset.Initialize(prototype);
|
||||
},
|
||||
() => _presetName);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
LoadBodyPreset(Preset);
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
// Just in case something activates at default health.
|
||||
ForceHealthChangedEvent();
|
||||
}
|
||||
|
||||
private void LoadBodyPreset(BodyPreset preset)
|
||||
{
|
||||
_presetName = preset.Name;
|
||||
|
||||
foreach (var slotName in Template.Slots.Keys)
|
||||
{
|
||||
// For each slot in our BodyManagerComponent's template,
|
||||
// try and grab what the ID of what the preset says should be inside it.
|
||||
if (!preset.PartIDs.TryGetValue(slotName, out var partId))
|
||||
{
|
||||
// If the preset doesn't define anything for it, continue.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the BodyPartPrototype corresponding to the BodyPart ID we grabbed.
|
||||
if (!_prototypeManager.TryIndex(partId, out BodyPartPrototype newPartData))
|
||||
{
|
||||
throw new InvalidOperationException($"No {nameof(BodyPartPrototype)} prototype found with ID {partId}");
|
||||
}
|
||||
|
||||
// Try and remove an existing limb if that exists.
|
||||
RemovePart(slotName, false);
|
||||
|
||||
// Add a new BodyPart with the BodyPartPrototype as a baseline to our
|
||||
// BodyComponent.
|
||||
var addedPart = new BodyPart(newPartData);
|
||||
TryAddPart(slotName, addedPart);
|
||||
}
|
||||
|
||||
OnBodyChanged(); // TODO: Duplicate code
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// Changes the current <see cref="BodyTemplate"/> to the given
|
||||
// /// <see cref="BodyTemplate"/>.
|
||||
// /// Attempts to keep previous <see cref="IBodyPart"/> if there is a
|
||||
// /// slot for them in both <see cref="BodyTemplate"/>.
|
||||
// /// </summary>
|
||||
// public void ChangeBodyTemplate(BodyTemplatePrototype newTemplate)
|
||||
// {
|
||||
// foreach (var part in Parts)
|
||||
// {
|
||||
// // TODO: Make this work.
|
||||
// }
|
||||
//
|
||||
// OnBodyChanged();
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// This method is called by <see cref="BodySystem.Update"/> before
|
||||
/// <see cref="MetabolismComponent.Update"/> is called.
|
||||
/// </summary>
|
||||
public void PreMetabolism(float frameTime)
|
||||
{
|
||||
if (CurrentDamageState == DamageState.Dead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var part in Parts.Values)
|
||||
{
|
||||
part.PreMetabolism(frameTime);
|
||||
}
|
||||
|
||||
foreach (var network in _networks.Values)
|
||||
{
|
||||
network.PreMetabolism(frameTime);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is called by <see cref="BodySystem.Update"/> after
|
||||
/// <see cref="MetabolismComponent.Update"/> is called.
|
||||
/// </summary>
|
||||
public void PostMetabolism(float frameTime)
|
||||
{
|
||||
if (CurrentDamageState == DamageState.Dead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var part in Parts.Values)
|
||||
{
|
||||
part.PostMetabolism(frameTime);
|
||||
}
|
||||
|
||||
foreach (var network in _networks.Values)
|
||||
{
|
||||
network.PostMetabolism(frameTime);
|
||||
}
|
||||
}
|
||||
|
||||
void IRelayMoveInput.MoveInputPressed(ICommonSession session)
|
||||
{
|
||||
if (CurrentDamageState == DamageState.Dead)
|
||||
{
|
||||
new Ghost().Execute(null, (IPlayerSession) session, null);
|
||||
}
|
||||
}
|
||||
|
||||
#region BodyNetwork Functions
|
||||
|
||||
private bool EnsureNetwork(BodyNetwork network)
|
||||
{
|
||||
DebugTools.AssertNotNull(network);
|
||||
|
||||
if (_networks.ContainsKey(network.GetType()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_networks.Add(network.GetType(), network);
|
||||
network.OnAdd(Owner);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to add a <see cref="BodyNetwork"/> of the given type to this body.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// True if successful, false if there was an error
|
||||
/// (such as passing in an invalid type or a network of that type already
|
||||
/// existing).
|
||||
/// </returns>
|
||||
public bool EnsureNetwork(Type networkType)
|
||||
{
|
||||
DebugTools.Assert(networkType.IsSubclassOf(typeof(BodyNetwork)));
|
||||
|
||||
var network = _bodyNetworkFactory.GetNetwork(networkType);
|
||||
return EnsureNetwork(network);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to add a <see cref="BodyNetwork"/> of the given type to
|
||||
/// this body.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of network to add.</typeparam>
|
||||
/// <returns>
|
||||
/// True if successful, false if there was an error
|
||||
/// (such as passing in an invalid type or a network of that type already
|
||||
/// existing).
|
||||
/// </returns>
|
||||
public bool EnsureNetwork<T>() where T : BodyNetwork
|
||||
{
|
||||
return EnsureNetwork(typeof(T));
|
||||
}
|
||||
|
||||
public void RemoveNetwork(Type networkType)
|
||||
{
|
||||
DebugTools.AssertNotNull(networkType);
|
||||
|
||||
if (_networks.Remove(networkType, out var network))
|
||||
{
|
||||
network.OnRemove();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveNetwork<T>() where T : BodyNetwork
|
||||
{
|
||||
RemoveNetwork(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get the <see cref="BodyNetwork"/> of the given type in this body.
|
||||
/// </summary>
|
||||
/// <param name="networkType">The type to search for.</param>
|
||||
/// <param name="result">
|
||||
/// The <see cref="BodyNetwork"/> if found, null otherwise.
|
||||
/// </param>
|
||||
/// <returns>True if found, false otherwise.</returns>
|
||||
public bool TryGetNetwork(Type networkType, [NotNullWhen(true)] out BodyNetwork result)
|
||||
{
|
||||
return _networks.TryGetValue(networkType, out result!);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public interface IBodyManagerHealthChangeParams
|
||||
{
|
||||
BodyPartType Part { get; }
|
||||
}
|
||||
|
||||
public class BodyManagerHealthChangeParams : HealthChangeParams, IBodyManagerHealthChangeParams
|
||||
{
|
||||
public BodyManagerHealthChangeParams(BodyPartType part)
|
||||
{
|
||||
Part = part;
|
||||
}
|
||||
|
||||
public BodyPartType Part { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
public interface IBodyHealthChangeParams
|
||||
{
|
||||
BodyPartType Part { get; }
|
||||
}
|
||||
|
||||
public class BodyHealthChangeParams : HealthChangeParams, IBodyHealthChangeParams
|
||||
{
|
||||
public BodyHealthChangeParams(BodyPartType part)
|
||||
{
|
||||
Part = part;
|
||||
}
|
||||
|
||||
public BodyPartType Part { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Body;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Body.Scanner;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Scanner;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects.Components.UserInterface;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
@@ -14,27 +13,32 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
public class BodyScannerComponent : Component, IActivate
|
||||
[ComponentReference(typeof(SharedBodyScannerComponent))]
|
||||
public class BodyScannerComponent : SharedBodyScannerComponent, IActivate
|
||||
{
|
||||
public sealed override string Name => "BodyScanner";
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BodyScannerUiKey.Key);
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor) ||
|
||||
actor.playerSession.AttachedEntity == null)
|
||||
if (!eventArgs.User.TryGetComponent(out IActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor.playerSession.AttachedEntity.TryGetComponent(out BodyManagerComponent? attempt))
|
||||
var session = actor.playerSession;
|
||||
|
||||
if (session.AttachedEntity == null)
|
||||
{
|
||||
var state = InterfaceState(attempt.Template, attempt.Parts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.AttachedEntity.TryGetComponent(out IBody? body))
|
||||
{
|
||||
var state = InterfaceState(body);
|
||||
UserInterface?.SetState(state);
|
||||
}
|
||||
|
||||
UserInterface?.Open(actor.playerSession);
|
||||
UserInterface?.Open(session);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
@@ -56,29 +60,9 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
/// <summary>
|
||||
/// Copy BodyTemplate and BodyPart data into a common data class that the client can read.
|
||||
/// </summary>
|
||||
private BodyScannerInterfaceState InterfaceState(BodyTemplate template, IReadOnlyDictionary<string, IBodyPart> bodyParts)
|
||||
private BodyScannerUIState InterfaceState(IBody body)
|
||||
{
|
||||
var partsData = new Dictionary<string, BodyScannerBodyPartData>();
|
||||
|
||||
foreach (var (slotName, part) in bodyParts)
|
||||
{
|
||||
var mechanismData = new List<BodyScannerMechanismData>();
|
||||
|
||||
foreach (var mechanism in part.Mechanisms)
|
||||
{
|
||||
mechanismData.Add(new BodyScannerMechanismData(mechanism.Name, mechanism.Description,
|
||||
mechanism.RSIPath,
|
||||
mechanism.RSIState, mechanism.MaxDurability, mechanism.CurrentDurability));
|
||||
}
|
||||
|
||||
partsData.Add(slotName,
|
||||
new BodyScannerBodyPartData(part.Name, part.RSIPath, part.RSIState, part.MaxDurability,
|
||||
part.CurrentDurability, mechanismData));
|
||||
}
|
||||
|
||||
var templateData = new BodyScannerTemplateData(template.Name, template.Slots);
|
||||
|
||||
return new BodyScannerInterfaceState(partsData, templateData);
|
||||
return new BodyScannerUIState(body.Owner.Uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Server.GameObjects.Components.Metabolism;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body.Networks;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -12,7 +13,8 @@ using Robust.Shared.ViewVariables;
|
||||
namespace Content.Server.GameObjects.Components.Body.Circulatory
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class BloodstreamComponent : Component, IGasMixtureHolder
|
||||
[ComponentReference(typeof(SharedBloodstreamComponent))]
|
||||
public class BloodstreamComponent : SharedBloodstreamComponent, IGasMixtureHolder
|
||||
{
|
||||
public override string Name => "Bloodstream";
|
||||
|
||||
@@ -58,7 +60,7 @@ namespace Content.Server.GameObjects.Components.Body.Circulatory
|
||||
/// </summary>
|
||||
/// <param name="solution">Solution to be transferred</param>
|
||||
/// <returns>Whether or not transfer was a success</returns>
|
||||
public bool TryTransferSolution(Solution solution)
|
||||
public override bool TryTransferSolution(Solution solution)
|
||||
{
|
||||
// For now doesn't support partial transfers
|
||||
if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Nutrition;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Digestive
|
||||
{
|
||||
/// <summary>
|
||||
/// Where reagents go when ingested. Tracks ingested reagents over time, and
|
||||
/// eventually transfers them to <see cref="BloodstreamComponent"/> once digested.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class StomachComponent : SharedStomachComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Max volume of internal solution storage
|
||||
/// </summary>
|
||||
public ReagentUnit MaxVolume
|
||||
{
|
||||
get => Owner.TryGetComponent(out SolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
|
||||
set
|
||||
{
|
||||
if (Owner.TryGetComponent(out SolutionContainerComponent? solution))
|
||||
{
|
||||
solution.MaxVolume = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initial internal solution storage volume
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private ReagentUnit _initialMaxVolume;
|
||||
|
||||
/// <summary>
|
||||
/// Time in seconds between reagents being ingested and them being transferred
|
||||
/// to <see cref="BloodstreamComponent"/>
|
||||
/// </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(ref _initialMaxVolume, "maxVolume", ReagentUnit.New(100));
|
||||
serializer.DataField(ref _digestionDelay, "digestionDelay", 20);
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
if (!Owner.EnsureComponent(out SolutionContainerComponent solution))
|
||||
{
|
||||
Logger.Warning($"Entity {Owner} at {Owner.Transform.MapPosition} didn't have a {nameof(SolutionContainerComponent)}");
|
||||
}
|
||||
|
||||
solution.MaxVolume = _initialMaxVolume;
|
||||
}
|
||||
|
||||
public bool CanTransferSolution(Solution solution)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? 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;
|
||||
|
||||
var solutionComponent = Owner.GetComponent<SolutionContainerComponent>();
|
||||
|
||||
// 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>
|
||||
/// 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 void Update(float frameTime)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent) ||
|
||||
!Owner.TryGetComponent(out BloodstreamComponent? 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)
|
||||
{
|
||||
solutionComponent.TryRemoveReagent(delta.ReagentId, delta.Quantity);
|
||||
transferSolution.AddReagent(delta.ReagentId, delta.Quantity);
|
||||
_reagentDeltas.Remove(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer digested reagents to bloodstream
|
||||
bloodstream.TryTransferSolution(transferSolution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to track quantity changes when ingesting & digesting reagents
|
||||
/// </summary>
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Body;
|
||||
using Content.Server.Body.Mechanisms;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Body.Mechanism;
|
||||
using Content.Shared.Body.Surgery;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.UserInterface;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
/// <summary>
|
||||
/// Component representing a dropped, tangible <see cref="Mechanism"/> entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class DroppedMechanismComponent : Component, IAfterInteract
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public sealed override string Name => "DroppedMechanism";
|
||||
|
||||
private readonly Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
|
||||
|
||||
private BodyManagerComponent? _bodyManagerComponentCache;
|
||||
|
||||
private int _idHash;
|
||||
|
||||
private IEntity? _performerCache;
|
||||
|
||||
[ViewVariables] public IMechanism ContainedMechanism { get; private set; } = default!;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GenericSurgeryUiKey.Key);
|
||||
|
||||
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseAllSurgeryUIs();
|
||||
_optionsCache.Clear();
|
||||
_performerCache = null;
|
||||
_bodyManagerComponentCache = null;
|
||||
|
||||
if (eventArgs.Target.TryGetComponent<BodyManagerComponent>(out var bodyManager))
|
||||
{
|
||||
SendBodyPartListToUser(eventArgs, bodyManager);
|
||||
}
|
||||
else if (eventArgs.Target.TryGetComponent<DroppedBodyPartComponent>(out var droppedBodyPart))
|
||||
{
|
||||
DebugTools.AssertNotNull(droppedBodyPart.ContainedBodyPart);
|
||||
|
||||
if (!droppedBodyPart.ContainedBodyPart.TryInstallDroppedMechanism(this))
|
||||
{
|
||||
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializeDroppedMechanism(IMechanism data)
|
||||
{
|
||||
ContainedMechanism = data;
|
||||
Owner.Name = Loc.GetString(ContainedMechanism.Name);
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? component))
|
||||
{
|
||||
component.LayerSetRSI(0, data.RSIPath);
|
||||
component.LayerSetState(0, data.RSIState);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
// This is a temporary way to have spawnable hard-coded DroppedMechanismComponent prototypes
|
||||
// In the future (when it becomes possible) DroppedMechanismComponent should be auto-generated from
|
||||
// the Mechanism prototypes
|
||||
var debugLoadMechanismData = "";
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref debugLoadMechanismData, "debugLoadMechanismData", "");
|
||||
|
||||
if (serializer.Reading && debugLoadMechanismData != "")
|
||||
{
|
||||
_prototypeManager.TryIndex(debugLoadMechanismData!, out MechanismPrototype data);
|
||||
|
||||
var mechanism = new Mechanism(data);
|
||||
mechanism.EnsureInitialize();
|
||||
|
||||
InitializeDroppedMechanism(mechanism);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
|
||||
{
|
||||
// Create dictionary to send to client (text to be shown : data sent back if selected)
|
||||
var toSend = new Dictionary<string, int>();
|
||||
|
||||
foreach (var (key, value) in bodyManager.Parts)
|
||||
{
|
||||
// For each limb in the target, add it to our cache if it is a valid option.
|
||||
if (value.CanInstallMechanism(ContainedMechanism))
|
||||
{
|
||||
_optionsCache.Add(_idHash, value);
|
||||
toSend.Add(key + ": " + value.Name, _idHash++);
|
||||
}
|
||||
}
|
||||
|
||||
if (_optionsCache.Count > 0)
|
||||
{
|
||||
OpenSurgeryUI(eventArgs.User.GetComponent<BasicActorComponent>().playerSession);
|
||||
UpdateSurgeryUIBodyPartRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession,
|
||||
toSend);
|
||||
_performerCache = eventArgs.User;
|
||||
_bodyManagerComponentCache = bodyManager;
|
||||
}
|
||||
else // If surgery cannot be performed, show message saying so.
|
||||
{
|
||||
eventArgs.Target.PopupMessage(eventArgs.User,
|
||||
Loc.GetString("You see no way to install the {0}.", Owner.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the client chooses from a list of possible BodyParts that can be operated on.
|
||||
/// </summary>
|
||||
private void HandleReceiveBodyPart(int key)
|
||||
{
|
||||
if (_performerCache == null ||
|
||||
!_performerCache.TryGetComponent(out IActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseSurgeryUI(actor.playerSession);
|
||||
|
||||
if (_bodyManagerComponentCache == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
|
||||
if (!_optionsCache.TryGetValue(key, out var targetObject))
|
||||
{
|
||||
_bodyManagerComponentCache.Owner.PopupMessage(_performerCache,
|
||||
Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
var target = (BodyPart) targetObject;
|
||||
var message = target.TryInstallDroppedMechanism(this)
|
||||
? Loc.GetString("You jam the {0} inside {1:them}.", ContainedMechanism.Name, _performerCache)
|
||||
: Loc.GetString("You can't fit it in!");
|
||||
|
||||
_bodyManagerComponentCache.Owner.PopupMessage(_performerCache, message);
|
||||
|
||||
// TODO: {1:theName}
|
||||
}
|
||||
|
||||
private void OpenSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Open(session);
|
||||
}
|
||||
|
||||
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
|
||||
{
|
||||
UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
|
||||
}
|
||||
|
||||
private void CloseSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Close(session);
|
||||
}
|
||||
|
||||
private void CloseAllSurgeryUIs()
|
||||
{
|
||||
UserInterface?.CloseAll();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
|
||||
{
|
||||
switch (message.Message)
|
||||
{
|
||||
case ReceiveBodyPartSurgeryUIMessage msg:
|
||||
HandleReceiveBodyPart(msg.SelectedOptionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
using System;
|
||||
using Content.Server.Body;
|
||||
using Content.Server.Body.Network;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
// TODO: Merge with ISharedBodyManagerComponent
|
||||
public interface IBodyManagerComponent : ISharedBodyManagerComponent, IBodyPartManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="BodyTemplate"/> that this
|
||||
/// <see cref="BodyManagerComponent"/> is adhering to.
|
||||
/// </summary>
|
||||
public BodyTemplate Template { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Installs 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>
|
||||
/// Ensures that this body has the specified network.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the network to ensure.</typeparam>
|
||||
/// <returns>
|
||||
/// True if the network already existed, false if it had to be created.
|
||||
/// </returns>
|
||||
bool EnsureNetwork<T>() where T : BodyNetwork;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that this body has the specified network.
|
||||
/// </summary>
|
||||
/// <param name="networkType">The type of the network to ensure.</param>
|
||||
/// <returns>
|
||||
/// True if the network already existed, false if it had to be created.
|
||||
/// </returns>
|
||||
bool EnsureNetwork(Type networkType);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the <see cref="BodyNetwork"/> of the given type in this body,
|
||||
/// if one exists.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the network to remove.</typeparam>
|
||||
void RemoveNetwork<T>() where T : BodyNetwork;
|
||||
|
||||
/// <summary>
|
||||
/// Removes the <see cref="BodyNetwork"/> of the given type in this body,
|
||||
/// if there is one.
|
||||
/// </summary>
|
||||
/// <param name="networkType">The type of the network to remove.</param>
|
||||
void RemoveNetwork(Type networkType);
|
||||
|
||||
void PreMetabolism(float frameTime);
|
||||
|
||||
void PostMetabolism(float frameTime);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Body;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
public interface IBodyPartManager : IComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="BodyPreset"/> that this
|
||||
/// <see cref="BodyManagerComponent"/>
|
||||
/// is adhering to.
|
||||
/// </summary>
|
||||
public BodyPreset Preset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Installs the given <see cref="DroppedBodyPartComponent"/> into the
|
||||
/// given slot, deleting the <see cref="IEntity"/> afterwards.
|
||||
/// </summary>
|
||||
/// <returns>True if successful, false otherwise.</returns>
|
||||
bool TryAddPart(string slot, DroppedBodyPartComponent part, bool force = false);
|
||||
|
||||
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>
|
||||
/// <returns>
|
||||
/// The <see cref="IEntity"/> representing the dropped
|
||||
/// <see cref="IBodyPart"/>, or null if none was dropped.
|
||||
/// </returns>
|
||||
IEntity? DropPart(IBodyPart part);
|
||||
|
||||
/// <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>
|
||||
/// Grabs all <see cref="IBodyPart"/> of the given type in this body.
|
||||
/// </summary>
|
||||
List<IBodyPart> GetPartsOfType(BodyPartType type);
|
||||
}
|
||||
}
|
||||
181
Content.Server/GameObjects/Components/Body/MechanismComponent.cs
Normal file
181
Content.Server/GameObjects/Components/Body/MechanismComponent.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Body.Surgery;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.Components.UserInterface;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedMechanismComponent))]
|
||||
[ComponentReference(typeof(IMechanism))]
|
||||
public class MechanismComponent : SharedMechanismComponent, IAfterInteract
|
||||
{
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += OnUIMessage;
|
||||
}
|
||||
}
|
||||
|
||||
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseAllSurgeryUIs();
|
||||
OptionsCache.Clear();
|
||||
PerformerCache = null;
|
||||
BodyCache = null;
|
||||
|
||||
if (eventArgs.Target.TryGetBody(out var body))
|
||||
{
|
||||
SendBodyPartListToUser(eventArgs, body);
|
||||
}
|
||||
else if (eventArgs.Target.TryGetComponent<IBodyPart>(out var part))
|
||||
{
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
if (!part.TryAddMechanism(this))
|
||||
{
|
||||
eventArgs.Target.PopupMessage(eventArgs.User, Loc.GetString("You can't fit it in!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBodyPartListToUser(AfterInteractEventArgs eventArgs, IBody body)
|
||||
{
|
||||
// Create dictionary to send to client (text to be shown : data sent back if selected)
|
||||
var toSend = new Dictionary<string, int>();
|
||||
|
||||
foreach (var (key, value) in body.Parts)
|
||||
{
|
||||
// For each limb in the target, add it to our cache if it is a valid option.
|
||||
if (value.CanAddMechanism(this))
|
||||
{
|
||||
OptionsCache.Add(IdHash, value);
|
||||
toSend.Add(key + ": " + value.Name, IdHash++);
|
||||
}
|
||||
}
|
||||
|
||||
if (OptionsCache.Count > 0 &&
|
||||
eventArgs.User.TryGetComponent(out IActorComponent? actor))
|
||||
{
|
||||
OpenSurgeryUI(actor.playerSession);
|
||||
UpdateSurgeryUIBodyPartRequest(actor.playerSession, toSend);
|
||||
PerformerCache = eventArgs.User;
|
||||
BodyCache = body;
|
||||
}
|
||||
else // If surgery cannot be performed, show message saying so.
|
||||
{
|
||||
eventArgs.Target.PopupMessage(eventArgs.User,
|
||||
Loc.GetString("You see no way to install the {0}.", Owner.Name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the client chooses from a list of possible BodyParts that can be operated on.
|
||||
/// </summary>
|
||||
private void HandleReceiveBodyPart(int key)
|
||||
{
|
||||
if (PerformerCache == null ||
|
||||
!PerformerCache.TryGetComponent(out IActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseSurgeryUI(actor.playerSession);
|
||||
|
||||
if (BodyCache == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
|
||||
if (!OptionsCache.TryGetValue(key, out var targetObject))
|
||||
{
|
||||
BodyCache.Owner.PopupMessage(PerformerCache,
|
||||
Loc.GetString("You see no useful way to use the {0} anymore.", Owner.Name));
|
||||
return;
|
||||
}
|
||||
|
||||
var target = (IBodyPart) targetObject;
|
||||
var message = target.TryAddMechanism(this)
|
||||
? Loc.GetString("You jam {0:theName} inside {1:them}.", Owner, PerformerCache)
|
||||
: Loc.GetString("You can't fit it in!");
|
||||
|
||||
BodyCache.Owner.PopupMessage(PerformerCache, message);
|
||||
|
||||
// TODO: {1:theName}
|
||||
}
|
||||
|
||||
private void OpenSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Open(session);
|
||||
}
|
||||
|
||||
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
|
||||
{
|
||||
UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
|
||||
}
|
||||
|
||||
private void CloseSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Close(session);
|
||||
}
|
||||
|
||||
private void CloseAllSurgeryUIs()
|
||||
{
|
||||
UserInterface?.CloseAll();
|
||||
}
|
||||
|
||||
private void OnUIMessage(ServerBoundUserInterfaceMessage message)
|
||||
{
|
||||
switch (message.Message)
|
||||
{
|
||||
case ReceiveBodyPartSurgeryUIMessage msg:
|
||||
HandleReceiveBodyPart(msg.SelectedOptionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPartAdd(IBodyPart? old, IBodyPart current)
|
||||
{
|
||||
base.OnPartAdd(old, current);
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPartRemove(IBodyPart old)
|
||||
{
|
||||
base.OnPartRemove(old);
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.Visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Body;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Body.Surgery;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Body.Surgery;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -13,42 +15,43 @@ using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
namespace Content.Server.GameObjects.Components.Body.Part
|
||||
{
|
||||
/// <summary>
|
||||
/// Component representing a dropped, tangible <see cref="BodyPart"/> entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class DroppedBodyPartComponent : Component, IAfterInteract, IBodyPartContainer
|
||||
[ComponentReference(typeof(SharedBodyPartComponent))]
|
||||
[ComponentReference(typeof(IBodyPart))]
|
||||
public class BodyPartComponent : SharedBodyPartComponent, IAfterInteract
|
||||
{
|
||||
private readonly Dictionary<int, object> _optionsCache = new Dictionary<int, object>();
|
||||
private BodyManagerComponent? _bodyManagerComponentCache;
|
||||
|
||||
private IBody? _owningBodyCache;
|
||||
|
||||
private int _idHash;
|
||||
private IEntity? _performerCache;
|
||||
|
||||
public sealed override string Name => "DroppedBodyPart";
|
||||
private IEntity? _surgeonCache;
|
||||
|
||||
[ViewVariables] public BodyPart ContainedBodyPart { get; private set; } = default!;
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GenericSurgeryUiKey.Key);
|
||||
|
||||
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
protected override void OnAddMechanism(IMechanism mechanism)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
base.OnAddMechanism(mechanism);
|
||||
|
||||
if (mechanism.Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
return;
|
||||
sprite.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
CloseAllSurgeryUIs();
|
||||
_optionsCache.Clear();
|
||||
_performerCache = null;
|
||||
_bodyManagerComponentCache = null;
|
||||
protected override void OnRemoveMechanism(IMechanism mechanism)
|
||||
{
|
||||
base.OnRemoveMechanism(mechanism);
|
||||
|
||||
if (eventArgs.Target.TryGetComponent(out BodyManagerComponent? bodyManager))
|
||||
if (mechanism.Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
SendBodySlotListToUser(eventArgs, bodyManager);
|
||||
sprite.Visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,49 +59,77 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// This is ran in Startup as entities spawned in Initialize
|
||||
// are not synced to the client since they are assumed to be
|
||||
// identical on it
|
||||
foreach (var mechanismId in MechanismIds)
|
||||
{
|
||||
var entity = Owner.EntityManager.SpawnEntity(mechanismId, Owner.Transform.MapPosition);
|
||||
|
||||
if (!entity.TryGetComponent(out IMechanism? mechanism))
|
||||
{
|
||||
Logger.Error($"Entity {mechanismId} does not have a {nameof(IMechanism)} component.");
|
||||
continue;
|
||||
}
|
||||
|
||||
TryAddMechanism(mechanism, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
UserInterface.OnReceiveMessage += OnUIMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public void TransferBodyPartData(BodyPart data)
|
||||
{
|
||||
ContainedBodyPart = data;
|
||||
Owner.Name = Loc.GetString(ContainedBodyPart.Name);
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? component))
|
||||
foreach (var mechanism in Mechanisms)
|
||||
{
|
||||
component.LayerSetRSI(0, data.RSIPath);
|
||||
component.LayerSetState(0, data.RSIState);
|
||||
|
||||
if (data.RSIColor.HasValue)
|
||||
{
|
||||
component.LayerSetColor(0, data.RSIColor.Value);
|
||||
}
|
||||
mechanism.Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBodySlotListToUser(AfterInteractEventArgs eventArgs, BodyManagerComponent bodyManager)
|
||||
public void AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
// TODO BODY
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseAllSurgeryUIs();
|
||||
_optionsCache.Clear();
|
||||
_surgeonCache = null;
|
||||
_owningBodyCache = null;
|
||||
|
||||
if (eventArgs.Target.TryGetBody(out var body))
|
||||
{
|
||||
SendSlots(eventArgs, body);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendSlots(AfterInteractEventArgs eventArgs, IBody body)
|
||||
{
|
||||
// Create dictionary to send to client (text to be shown : data sent back if selected)
|
||||
var toSend = new Dictionary<string, int>();
|
||||
|
||||
// Here we are trying to grab a list of all empty BodySlots adjacent to an existing BodyPart that can be
|
||||
// attached to. i.e. an empty left hand slot, connected to an occupied left arm slot would be valid.
|
||||
var unoccupiedSlots = bodyManager.AllSlots.ToList().Except(bodyManager.OccupiedSlots.ToList()).ToList();
|
||||
var unoccupiedSlots = body.Slots.Keys.ToList().Except(body.Parts.Keys.ToList()).ToList();
|
||||
foreach (var slot in unoccupiedSlots)
|
||||
{
|
||||
if (!bodyManager.TryGetSlotType(slot, out var typeResult) ||
|
||||
typeResult != ContainedBodyPart?.PartType ||
|
||||
!bodyManager.TryGetPartConnections(slot, out var parts))
|
||||
if (!body.TryGetSlotType(slot, out var typeResult) ||
|
||||
typeResult != PartType ||
|
||||
!body.TryGetPartConnections(slot, out var parts))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var connectedPart in parts)
|
||||
{
|
||||
if (!connectedPart.CanAttachPart(ContainedBodyPart))
|
||||
if (!connectedPart.CanAttachPart(this))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -111,10 +142,10 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
if (_optionsCache.Count > 0)
|
||||
{
|
||||
OpenSurgeryUI(eventArgs.User.GetComponent<BasicActorComponent>().playerSession);
|
||||
UpdateSurgeryUIBodyPartSlotRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession,
|
||||
BodyPartSlotRequest(eventArgs.User.GetComponent<BasicActorComponent>().playerSession,
|
||||
toSend);
|
||||
_performerCache = eventArgs.User;
|
||||
_bodyManagerComponentCache = bodyManager;
|
||||
_surgeonCache = eventArgs.User;
|
||||
_owningBodyCache = body;
|
||||
}
|
||||
else // If surgery cannot be performed, show message saying so.
|
||||
{
|
||||
@@ -124,19 +155,20 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the client chooses from a list of possible BodyPartSlots to install the limb on.
|
||||
/// Called after the client chooses from a list of possible
|
||||
/// BodyPartSlots to install the limb on.
|
||||
/// </summary>
|
||||
private void HandleReceiveBodyPartSlot(int key)
|
||||
private void ReceiveBodyPartSlot(int key)
|
||||
{
|
||||
if (_performerCache == null ||
|
||||
!_performerCache.TryGetComponent(out IActorComponent? actor))
|
||||
if (_surgeonCache == null ||
|
||||
!_surgeonCache.TryGetComponent(out IActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseSurgeryUI(actor.playerSession);
|
||||
|
||||
if (_bodyManagerComponentCache == null)
|
||||
if (_owningBodyCache == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -144,23 +176,16 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
|
||||
if (!_optionsCache.TryGetValue(key, out var targetObject))
|
||||
{
|
||||
_bodyManagerComponentCache.Owner.PopupMessage(_performerCache,
|
||||
_owningBodyCache.Owner.PopupMessage(_surgeonCache,
|
||||
Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner));
|
||||
}
|
||||
|
||||
var target = (string) targetObject!;
|
||||
string message;
|
||||
var message = _owningBodyCache.TryAddPart(target, this)
|
||||
? Loc.GetString("You attach {0:theName}.", Owner)
|
||||
: Loc.GetString("You can't attach {0:theName}!", Owner);
|
||||
|
||||
if (_bodyManagerComponentCache.TryAddPart(target, this))
|
||||
{
|
||||
message = Loc.GetString("You attach {0:theName}.", ContainedBodyPart);
|
||||
}
|
||||
else
|
||||
{
|
||||
message = Loc.GetString("You can't attach it!");
|
||||
}
|
||||
|
||||
_bodyManagerComponentCache.Owner.PopupMessage(_performerCache, message);
|
||||
_owningBodyCache.Owner.PopupMessage(_surgeonCache, message);
|
||||
}
|
||||
|
||||
private void OpenSurgeryUI(IPlayerSession session)
|
||||
@@ -168,7 +193,7 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
UserInterface?.Open(session);
|
||||
}
|
||||
|
||||
private void UpdateSurgeryUIBodyPartSlotRequest(IPlayerSession session, Dictionary<string, int> options)
|
||||
private void BodyPartSlotRequest(IPlayerSession session, Dictionary<string, int> options)
|
||||
{
|
||||
UserInterface?.SendMessage(new RequestBodyPartSlotSurgeryUIMessage(options), session);
|
||||
}
|
||||
@@ -183,12 +208,12 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
UserInterface?.CloseAll();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
|
||||
private void OnUIMessage(ServerBoundUserInterfaceMessage message)
|
||||
{
|
||||
switch (message.Message)
|
||||
{
|
||||
case ReceiveBodyPartSlotSurgeryUIMessage msg:
|
||||
HandleReceiveBodyPartSlot(msg.SelectedOptionId);
|
||||
ReceiveBodyPartSlot(msg.SelectedOptionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Body;
|
||||
using Content.Server.Body.Mechanisms;
|
||||
using Content.Server.Body.Surgery;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Body.Surgery;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Body.Surgery;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -19,13 +18,10 @@ using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
// TODO: add checks to close UI if user walks too far away from tool or target.
|
||||
|
||||
/// <summary>
|
||||
/// Server-side component representing a generic tool capable of performing surgery.
|
||||
/// For instance, the scalpel.
|
||||
@@ -40,7 +36,7 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
|
||||
private float _baseOperateTime;
|
||||
|
||||
private BodyManagerComponent? _bodyManagerComponentCache;
|
||||
private IBody? _bodyCache;
|
||||
|
||||
private ISurgeon.MechanismRequestCallback? _callbackCache;
|
||||
|
||||
@@ -50,7 +46,7 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
|
||||
private SurgeryType _surgeryType;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GenericSurgeryUiKey.Key);
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
|
||||
|
||||
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
@@ -68,11 +64,11 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
_optionsCache.Clear();
|
||||
|
||||
_performerCache = null;
|
||||
_bodyManagerComponentCache = null;
|
||||
_bodyCache = null;
|
||||
_callbackCache = null;
|
||||
|
||||
// Attempt surgery on a BodyManagerComponent by sending a list of operable BodyParts to the client to choose from
|
||||
if (eventArgs.Target.TryGetComponent(out BodyManagerComponent? body))
|
||||
// Attempt surgery on a body by sending a list of operable parts for the client to choose from
|
||||
if (eventArgs.Target.TryGetBody(out var body))
|
||||
{
|
||||
// Create dictionary to send to client (text to be shown : data sent back if selected)
|
||||
var toSend = new Dictionary<string, int>();
|
||||
@@ -92,36 +88,34 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
OpenSurgeryUI(actor.playerSession);
|
||||
UpdateSurgeryUIBodyPartRequest(actor.playerSession, toSend);
|
||||
_performerCache = eventArgs.User; // Also, cache the data.
|
||||
_bodyManagerComponentCache = body;
|
||||
_bodyCache = body;
|
||||
}
|
||||
else // If surgery cannot be performed, show message saying so.
|
||||
{
|
||||
SendNoUsefulWayToUsePopup();
|
||||
NotUsefulPopup();
|
||||
}
|
||||
}
|
||||
else if (eventArgs.Target.TryGetComponent<DroppedBodyPartComponent>(out var droppedBodyPart))
|
||||
else if (eventArgs.Target.TryGetComponent<IBodyPart>(out var part))
|
||||
{
|
||||
// Attempt surgery on a DroppedBodyPart - there's only one possible target so no need for selection UI
|
||||
_performerCache = eventArgs.User;
|
||||
|
||||
DebugTools.AssertNotNull(droppedBodyPart.ContainedBodyPart);
|
||||
|
||||
// If surgery can be performed...
|
||||
if (!droppedBodyPart.ContainedBodyPart.SurgeryCheck(_surgeryType))
|
||||
if (!part.SurgeryCheck(_surgeryType))
|
||||
{
|
||||
SendNoUsefulWayToUsePopup();
|
||||
NotUsefulPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
//...do the surgery.
|
||||
if (droppedBodyPart.ContainedBodyPart.AttemptSurgery(_surgeryType, droppedBodyPart, this,
|
||||
// ...do the surgery.
|
||||
if (part.AttemptSurgery(_surgeryType, part, this,
|
||||
eventArgs.User))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Log error if the surgery fails somehow.
|
||||
Logger.Debug($"Error when trying to perform surgery on ${nameof(BodyPart)} {eventArgs.User.Name}");
|
||||
Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
@@ -161,6 +155,7 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
}
|
||||
}
|
||||
|
||||
// TODO BODY add checks to close UI if user walks too far away from tool or target.
|
||||
private void OpenSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Open(session);
|
||||
@@ -201,7 +196,7 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
|
||||
/// <summary>
|
||||
/// Called after the client chooses from a list of possible
|
||||
/// <see cref="BodyPart"/> that can be operated on.
|
||||
/// <see cref="IBodyPart"/> that can be operated on.
|
||||
/// </summary>
|
||||
private void HandleReceiveBodyPart(int key)
|
||||
{
|
||||
@@ -214,17 +209,18 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
CloseSurgeryUI(actor.playerSession);
|
||||
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
|
||||
if (!_optionsCache.TryGetValue(key, out var targetObject) ||
|
||||
_bodyManagerComponentCache == null)
|
||||
_bodyCache == null)
|
||||
{
|
||||
SendNoUsefulWayToUseAnymorePopup();
|
||||
NotUsefulAnymorePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
var target = (BodyPart) targetObject!;
|
||||
var target = (IBodyPart) targetObject!;
|
||||
|
||||
if (!target.AttemptSurgery(_surgeryType, _bodyManagerComponentCache, this, _performerCache))
|
||||
// TODO BODY Reconsider
|
||||
if (!target.AttemptSurgery(_surgeryType, _bodyCache, this, _performerCache))
|
||||
{
|
||||
SendNoUsefulWayToUseAnymorePopup();
|
||||
NotUsefulAnymorePopup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,25 +235,25 @@ namespace Content.Server.GameObjects.Components.Body
|
||||
_performerCache == null ||
|
||||
!_performerCache.TryGetComponent(out IActorComponent? actor))
|
||||
{
|
||||
SendNoUsefulWayToUseAnymorePopup();
|
||||
NotUsefulAnymorePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
var target = targetObject as Mechanism;
|
||||
var target = targetObject as MechanismComponent;
|
||||
|
||||
CloseSurgeryUI(actor.playerSession);
|
||||
_callbackCache?.Invoke(target, _bodyManagerComponentCache, this, _performerCache);
|
||||
_callbackCache?.Invoke(target, _bodyCache, this, _performerCache);
|
||||
}
|
||||
|
||||
private void SendNoUsefulWayToUsePopup()
|
||||
private void NotUsefulPopup()
|
||||
{
|
||||
_bodyManagerComponentCache?.Owner.PopupMessage(_performerCache,
|
||||
_bodyCache?.Owner.PopupMessage(_performerCache,
|
||||
Loc.GetString("You see no useful way to use {0:theName}.", Owner));
|
||||
}
|
||||
|
||||
private void SendNoUsefulWayToUseAnymorePopup()
|
||||
private void NotUsefulAnymorePopup()
|
||||
{
|
||||
_bodyManagerComponentCache?.Owner.PopupMessage(_performerCache,
|
||||
_bodyCache?.Owner.PopupMessage(_performerCache,
|
||||
Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Content.Server.GameObjects.Components.Body.Digestive;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body.Behavior;
|
||||
using Content.Server.GameObjects.Components.Nutrition;
|
||||
using Content.Server.GameObjects.Components.Utensil;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
@@ -80,7 +82,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
|
||||
var trueTarget = target ?? user;
|
||||
|
||||
if (!trueTarget.TryGetComponent(out StomachComponent stomach))
|
||||
if (!trueTarget.TryGetMechanismBehaviors<StomachBehaviorComponent>(out var stomachs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -93,7 +95,9 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
var transferAmount = ReagentUnit.Min(_transferAmount, _contents.CurrentVolume);
|
||||
var split = _contents.SplitSolution(transferAmount);
|
||||
|
||||
if (!stomach.CanTransferSolution(split))
|
||||
var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split));
|
||||
|
||||
if (firstStomach == null)
|
||||
{
|
||||
_contents.TryAddSolution(split);
|
||||
trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!"));
|
||||
@@ -108,7 +112,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
split.RemoveReagent(reagentId, reagent.ReactionEntity(trueTarget, ReactionMethod.Ingestion, quantity));
|
||||
}
|
||||
|
||||
stomach.TryTransferSolution(split);
|
||||
firstStomach.TryTransferSolution(split);
|
||||
|
||||
if (_useSound != null)
|
||||
{
|
||||
|
||||
@@ -1,29 +1,12 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Body.Digestive;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
/// ECS component that manages a liquid solution of reagents.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedSolutionContainerComponent))]
|
||||
public class SolutionContainerComponent : SharedSolutionContainerComponent, IExamine
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
@@ -42,42 +43,12 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
private ChemistrySystem _chemistrySystem;
|
||||
private SpriteComponent _spriteComponent;
|
||||
|
||||
/// <summary>
|
||||
/// The total volume of all the of the reagents in the container.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public ReagentUnit CurrentVolume => Solution.TotalVolume;
|
||||
|
||||
/// <summary>
|
||||
/// The volume without reagents remaining in the container.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume;
|
||||
|
||||
/// <summary>
|
||||
/// The current blended color of all the reagents in the container.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Color SubstanceColor { get; private set; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// The contained solution.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public Solution Solution { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum volume of the container.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public ReagentUnit MaxVolume { get; set; }
|
||||
|
||||
public IReadOnlyList<Solution.ReagentQuantity> ReagentList => Solution.Contents;
|
||||
public bool CanExamineContents => (Capabilities & SolutionContainerCaps.NoExamine) == 0;
|
||||
public bool CanUseWithChemDispenser => (Capabilities & SolutionContainerCaps.FitsInDispenser) != 0;
|
||||
@@ -124,7 +95,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
OnSolutionChanged(false);
|
||||
}
|
||||
|
||||
public bool TryRemoveReagent(string reagentId, ReagentUnit quantity)
|
||||
public override bool TryRemoveReagent(string reagentId, ReagentUnit quantity)
|
||||
{
|
||||
if (!ContainsReagent(reagentId, out var currentQuantity))
|
||||
{
|
||||
@@ -393,12 +364,12 @@ namespace Content.Server.GameObjects.Components.Chemistry
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CanAddSolution(Solution solution)
|
||||
public override bool CanAddSolution(Solution solution)
|
||||
{
|
||||
return solution.TotalVolume <= (MaxVolume - Solution.TotalVolume);
|
||||
}
|
||||
|
||||
public bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false)
|
||||
public override bool TryAddSolution(Solution solution, bool skipReactionCheck = false, bool skipColor = false)
|
||||
{
|
||||
if (!CanAddSolution(solution))
|
||||
return false;
|
||||
|
||||
@@ -27,13 +27,10 @@ namespace Content.Server.GameObjects.Components.Damage
|
||||
public override string Name => "Breakable";
|
||||
|
||||
private ActSystem _actSystem;
|
||||
private DamageState _currentDamageState;
|
||||
|
||||
public override List<DamageState> SupportedDamageStates =>
|
||||
new List<DamageState> {DamageState.Alive, DamageState.Dead};
|
||||
|
||||
public override DamageState CurrentDamageState => _currentDamageState;
|
||||
|
||||
void IExAct.OnExplosion(ExplosionEventArgs eventArgs)
|
||||
{
|
||||
switch (eventArgs.Severity)
|
||||
@@ -62,7 +59,7 @@ namespace Content.Server.GameObjects.Components.Damage
|
||||
public void FixAllDamage()
|
||||
{
|
||||
Heal();
|
||||
_currentDamageState = DamageState.Alive;
|
||||
CurrentState = DamageState.Alive;
|
||||
}
|
||||
|
||||
protected override void DestructionBehavior()
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Damage
|
||||
{
|
||||
@@ -15,18 +16,15 @@ namespace Content.Server.GameObjects.Components.Damage
|
||||
[ComponentReference(typeof(IDamageableComponent))]
|
||||
public abstract class RuinableComponent : DamageableComponent
|
||||
{
|
||||
private DamageState _currentDamageState;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played upon destruction.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
protected string DestroySound { get; private set; }
|
||||
|
||||
public override List<DamageState> SupportedDamageStates =>
|
||||
new List<DamageState> {DamageState.Alive, DamageState.Dead};
|
||||
|
||||
public override DamageState CurrentDamageState => _currentDamageState;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
@@ -34,8 +32,16 @@ namespace Content.Server.GameObjects.Components.Damage
|
||||
serializer.DataReadWriteFunction(
|
||||
"deadThreshold",
|
||||
100,
|
||||
t => DeadThreshold = t ,
|
||||
() => DeadThreshold ?? -1);
|
||||
t =>
|
||||
{
|
||||
if (t == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Thresholds[DamageState.Dead] = t.Value;
|
||||
},
|
||||
() => Thresholds.TryGetValue(DamageState.Dead, out var value) ? value : (int?) null);
|
||||
|
||||
serializer.DataField(this, ruinable => ruinable.DestroySound, "destroySound", string.Empty);
|
||||
}
|
||||
@@ -52,12 +58,12 @@ namespace Content.Server.GameObjects.Components.Damage
|
||||
|
||||
/// <summary>
|
||||
/// Destroys the Owner <see cref="IEntity"/>, setting
|
||||
/// <see cref="IDamageableComponent.CurrentDamageState"/> to
|
||||
/// <see cref="DamageState.Dead"/>
|
||||
/// <see cref="IDamageableComponent.CurrentState"/> to
|
||||
/// <see cref="Shared.GameObjects.Components.Damage.DamageState.Dead"/>
|
||||
/// </summary>
|
||||
protected void PerformDestruction()
|
||||
{
|
||||
_currentDamageState = DamageState.Dead;
|
||||
CurrentState = DamageState.Dead;
|
||||
|
||||
if (!Owner.Deleted && DestroySound != string.Empty)
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Content.Server.GameObjects.Components.Disposal
|
||||
}
|
||||
|
||||
return entity.HasComponent<ItemComponent>() ||
|
||||
entity.HasComponent<ISharedBodyManagerComponent>();
|
||||
entity.HasComponent<IBody>();
|
||||
}
|
||||
|
||||
public bool TryInsert(IEntity entity)
|
||||
|
||||
@@ -145,7 +145,7 @@ namespace Content.Server.GameObjects.Components.Disposal
|
||||
}
|
||||
|
||||
if (!entity.HasComponent<ItemComponent>() &&
|
||||
!entity.HasComponent<ISharedBodyManagerComponent>())
|
||||
!entity.HasComponent<IBody>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Content.Server.GameObjects.Components.Doors
|
||||
/// Whether something is currently using a welder on this so DoAfter isn't spammed.
|
||||
/// </summary>
|
||||
private bool _beingWelded = false;
|
||||
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
private bool _canCrush = true;
|
||||
|
||||
@@ -147,7 +147,7 @@ namespace Content.Server.GameObjects.Components.Doors
|
||||
|
||||
// Disabled because it makes it suck hard to walk through double doors.
|
||||
|
||||
if (entity.HasComponent<ISharedBodyManagerComponent>())
|
||||
if (entity.HasComponent<IBody>())
|
||||
{
|
||||
if (!entity.TryGetComponent<IMoverComponent>(out var mover)) return;
|
||||
|
||||
@@ -315,7 +315,7 @@ namespace Content.Server.GameObjects.Components.Doors
|
||||
|
||||
damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner);
|
||||
stun.Paralyze(DoorStunTime);
|
||||
|
||||
|
||||
// If we hit someone, open up after stun (opens right when stun ends)
|
||||
Timer.Spawn(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open);
|
||||
break;
|
||||
@@ -479,7 +479,7 @@ namespace Content.Server.GameObjects.Components.Doors
|
||||
|
||||
if (_beingWelded)
|
||||
return false;
|
||||
|
||||
|
||||
_beingWelded = true;
|
||||
|
||||
if (!await tool.UseTool(eventArgs.User, Owner, 3f, ToolQuality.Welding, 3f, () => _canWeldShut))
|
||||
@@ -487,7 +487,7 @@ namespace Content.Server.GameObjects.Components.Doors
|
||||
_beingWelded = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
_beingWelded = false;
|
||||
IsWeldedShut ^= true;
|
||||
return true;
|
||||
|
||||
@@ -7,9 +7,9 @@ using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Server.GameObjects.EntitySystems.Click;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Interaction;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Items;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
@@ -725,24 +725,24 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
}
|
||||
}
|
||||
|
||||
void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs eventArgs)
|
||||
void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs args)
|
||||
{
|
||||
if (eventArgs.Part.PartType != BodyPartType.Hand)
|
||||
if (args.Part.PartType != BodyPartType.Hand)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddHand(eventArgs.SlotName);
|
||||
AddHand(args.Slot);
|
||||
}
|
||||
|
||||
void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs eventArgs)
|
||||
void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs args)
|
||||
{
|
||||
if (eventArgs.Part.PartType != BodyPartType.Hand)
|
||||
if (args.Part.PartType != BodyPartType.Hand)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RemoveHand(eventArgs.SlotName);
|
||||
RemoveHand(args.Slot);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
public string? WeldSoundCollection { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public float Fuel => _solutionComponent?.Solution.GetReagentQuantity("chem.WeldingFuel").Float() ?? 0f;
|
||||
public float Fuel => _solutionComponent?.Solution?.GetReagentQuantity("chem.WeldingFuel").Float() ?? 0f;
|
||||
|
||||
[ViewVariables]
|
||||
public float FuelCapacity => _solutionComponent?.MaxVolume.Float() ?? 0f;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Interactable;
|
||||
using Content.Shared.GameObjects.Components.Storage;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
@@ -171,7 +172,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
|
||||
|
||||
// only items that can be stored in an inventory, or a mob, can be eaten by a locker
|
||||
if (!entity.HasComponent<StorableComponent>() &&
|
||||
!entity.HasComponent<BodyManagerComponent>())
|
||||
!entity.HasComponent<IBody>())
|
||||
continue;
|
||||
|
||||
if (!AddToContents(entity))
|
||||
|
||||
@@ -3,7 +3,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
@@ -14,6 +13,7 @@ using Content.Server.Interfaces.GameObjects;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Power;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
@@ -477,27 +477,37 @@ namespace Content.Server.GameObjects.Components.Kitchen
|
||||
public SuicideKind Suicide(IEntity victim, IChatManager chat)
|
||||
{
|
||||
var headCount = 0;
|
||||
if (victim.TryGetComponent<BodyManagerComponent>(out var bodyManagerComponent))
|
||||
|
||||
if (victim.TryGetComponent<IBody>(out var body))
|
||||
{
|
||||
var heads = bodyManagerComponent.GetPartsOfType(BodyPartType.Head);
|
||||
var heads = body.GetPartsOfType(BodyPartType.Head);
|
||||
foreach (var head in heads)
|
||||
{
|
||||
var droppedHead = bodyManagerComponent.DropPart(head);
|
||||
|
||||
if (droppedHead == null)
|
||||
if (!body.TryDropPart(head, out var dropped))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_storage.Insert(droppedHead);
|
||||
headCount++;
|
||||
var droppedHeads = dropped.Where(p => p.PartType == BodyPartType.Head);
|
||||
|
||||
foreach (var droppedHead in droppedHeads)
|
||||
{
|
||||
_storage.Insert(droppedHead.Owner);
|
||||
headCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var othersMessage = Loc.GetString("{0:theName} is trying to cook {0:their} head!", victim);
|
||||
var othersMessage = headCount > 1
|
||||
? Loc.GetString("{0:theName} is trying to cook {0:their} heads!", victim)
|
||||
: Loc.GetString("{0:theName} is trying to cook {0:their} head!", victim);
|
||||
|
||||
victim.PopupMessageOtherClients(othersMessage);
|
||||
|
||||
var selfMessage = Loc.GetString("You cook your head!");
|
||||
var selfMessage = headCount > 1
|
||||
? Loc.GetString("You cook your heads!")
|
||||
: Loc.GetString("You cook your head!");
|
||||
|
||||
victim.PopupMessage(selfMessage);
|
||||
|
||||
_currentCookTimerTime = 10;
|
||||
|
||||
@@ -163,7 +163,7 @@ namespace Content.Server.GameObjects.Components.Medical
|
||||
|
||||
var dead =
|
||||
mind.OwnedEntity.TryGetComponent<IDamageableComponent>(out var damageable) &&
|
||||
damageable.CurrentDamageState == DamageState.Dead;
|
||||
damageable.CurrentState == DamageState.Dead;
|
||||
if (!dead) return;
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Stack;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
@@ -31,7 +31,7 @@ namespace Content.Server.GameObjects.Components.Medical
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventArgs.Target.TryGetComponent(out ISharedBodyManagerComponent body))
|
||||
if (!eventArgs.Target.TryGetComponent(out IDamageableComponent damageable))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace Content.Server.GameObjects.Components.Medical
|
||||
|
||||
foreach (var (type, amount) in Heal)
|
||||
{
|
||||
body.ChangeDamage(type, -amount, true);
|
||||
damageable.ChangeDamage(type, -amount, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.Components.Medical;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
@@ -128,7 +129,7 @@ namespace Content.Server.GameObjects.Components.Medical
|
||||
var body = _bodyContainer.ContainedEntity;
|
||||
return body == null
|
||||
? MedicalScannerStatus.Open
|
||||
: GetStatusFromDamageState(body.GetComponent<IDamageableComponent>().CurrentDamageState);
|
||||
: GetStatusFromDamageState(body.GetComponent<IDamageableComponent>().CurrentState);
|
||||
}
|
||||
|
||||
return MedicalScannerStatus.Off;
|
||||
@@ -249,7 +250,7 @@ namespace Content.Server.GameObjects.Components.Medical
|
||||
|
||||
public bool CanDragDropOn(DragDropEventArgs eventArgs)
|
||||
{
|
||||
return eventArgs.Dropped.HasComponent<BodyManagerComponent>();
|
||||
return eventArgs.Dropped.HasComponent<IBody>();
|
||||
}
|
||||
|
||||
public bool DragDropOn(DragDropEventArgs eventArgs)
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Body.Behavior;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.GameObjects.Components.Body.Respiratory;
|
||||
using Content.Server.GameObjects.Components.Temperature;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
@@ -190,9 +191,13 @@ namespace Content.Server.GameObjects.Components.Metabolism
|
||||
if (bloodstreamAmount < amountNeeded)
|
||||
{
|
||||
// Panic inhale
|
||||
if (Owner.TryGetComponent(out LungComponent lung))
|
||||
if (Owner.TryGetMechanismBehaviors(out List<LungBehaviorComponent> lungs))
|
||||
{
|
||||
lung.Gasp();
|
||||
foreach (var lung in lungs)
|
||||
{
|
||||
lung.Gasp();
|
||||
}
|
||||
|
||||
bloodstreamAmount = bloodstream.Air.GetMoles(gas);
|
||||
}
|
||||
|
||||
@@ -341,7 +346,7 @@ namespace Content.Server.GameObjects.Components.Metabolism
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
if (!Owner.TryGetComponent<IDamageableComponent>(out var damageable) ||
|
||||
damageable.CurrentDamageState == DamageState.Dead)
|
||||
damageable.CurrentState == DamageState.Dead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Mobs
|
||||
@@ -6,6 +9,44 @@ namespace Content.Server.GameObjects.Components.Mobs
|
||||
[RegisterComponent]
|
||||
public sealed class HumanoidAppearanceComponent : SharedHumanoidAppearanceComponent
|
||||
{
|
||||
public override HumanoidCharacterAppearance Appearance
|
||||
{
|
||||
get => base.Appearance;
|
||||
set
|
||||
{
|
||||
base.Appearance = value;
|
||||
|
||||
if (Owner.TryGetBody(out var body))
|
||||
{
|
||||
foreach (var part in body.Parts.Values)
|
||||
{
|
||||
if (!part.Owner.TryGetComponent(out SpriteComponent sprite))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sprite.Color = value.SkinColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
if (Appearance != null && Owner.TryGetBody(out var body))
|
||||
{
|
||||
foreach (var part in body.Parts.Values)
|
||||
{
|
||||
if (!part.Owner.TryGetComponent(out SpriteComponent sprite))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sprite.Color = Appearance.SkinColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ namespace Content.Server.GameObjects.Components.Mobs
|
||||
|
||||
var dead =
|
||||
Owner.TryGetComponent<IDamageableComponent>(out var damageable) &&
|
||||
damageable.CurrentDamageState == DamageState.Dead;
|
||||
damageable.CurrentState == DamageState.Dead;
|
||||
|
||||
if (!HasMind)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Server.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.Components.Mobs.State;
|
||||
@@ -41,26 +42,12 @@ namespace Content.Server.GameObjects.Components.Mobs.State
|
||||
{
|
||||
case RuinableComponent ruinable:
|
||||
{
|
||||
if (ruinable.DeadThreshold == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var modifier = (int) (ruinable.TotalDamage / (ruinable.DeadThreshold / 7f));
|
||||
|
||||
status.ChangeStatusEffectIcon(StatusEffect.Health,
|
||||
"/Textures/Interface/StatusEffects/Human/human" + modifier + ".png");
|
||||
|
||||
break;
|
||||
}
|
||||
case BodyManagerComponent body:
|
||||
{
|
||||
if (body.CriticalThreshold == null)
|
||||
if (!ruinable.Thresholds.TryGetValue(DamageState.Dead, out var threshold))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var modifier = (int) (body.TotalDamage / (body.CriticalThreshold / 7f));
|
||||
var modifier = (int) (ruinable.TotalDamage / (threshold / 7f));
|
||||
|
||||
status.ChangeStatusEffectIcon(StatusEffect.Health,
|
||||
"/Textures/Interface/StatusEffects/Human/human" + modifier + ".png");
|
||||
@@ -69,8 +56,15 @@ namespace Content.Server.GameObjects.Components.Mobs.State
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (!damageable.Thresholds.TryGetValue(DamageState.Critical, out var threshold))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var modifier = (int) (damageable.TotalDamage / (threshold / 7f));
|
||||
|
||||
status.ChangeStatusEffectIcon(StatusEffect.Health,
|
||||
"/Textures/Interface/StatusEffects/Human/human0.png");
|
||||
"/Textures/Interface/StatusEffects/Human/human" + modifier + ".png");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
@@ -92,16 +93,15 @@ namespace Content.Server.GameObjects.Components.Movement
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.HasComponent<ClimbingComponent>())
|
||||
if (!user.HasComponent<ClimbingComponent>() ||
|
||||
!user.TryGetComponent(out IBody body))
|
||||
{
|
||||
reason = Loc.GetString("You are incapable of climbing!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var bodyManager = user.GetComponent<BodyManagerComponent>();
|
||||
|
||||
if (bodyManager.GetPartsOfType(BodyPartType.Leg).Count == 0 ||
|
||||
bodyManager.GetPartsOfType(BodyPartType.Foot).Count == 0)
|
||||
if (body.GetPartsOfType(BodyPartType.Leg).Count == 0 ||
|
||||
body.GetPartsOfType(BodyPartType.Foot).Count == 0)
|
||||
{
|
||||
reason = Loc.GetString("You are unable to climb!");
|
||||
return false;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Content.Server.GameObjects.Components.Body.Digestive;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body.Behavior;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Server.GameObjects.Components.Fluids;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Nutrition;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
@@ -149,42 +151,43 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!target.TryGetComponent(out StomachComponent stomachComponent))
|
||||
if (!target.TryGetMechanismBehaviors<StomachBehaviorComponent>(out var stomachs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var transferAmount = ReagentUnit.Min(TransferAmount, _contents.CurrentVolume);
|
||||
var split = _contents.SplitSolution(transferAmount);
|
||||
var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split));
|
||||
|
||||
if (stomachComponent.CanTransferSolution(split))
|
||||
|
||||
// All stomach are full or can't handle whatever solution we have.
|
||||
if (firstStomach == null)
|
||||
{
|
||||
if (_useSound == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f));
|
||||
target.PopupMessage(Loc.GetString("Slurp"));
|
||||
UpdateAppearance();
|
||||
|
||||
// TODO: Account for partial transfer.
|
||||
|
||||
foreach (var (reagentId, quantity) in split.Contents)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype reagent)) continue;
|
||||
split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity));
|
||||
}
|
||||
|
||||
stomachComponent.TryTransferSolution(split);
|
||||
|
||||
return true;
|
||||
_contents.TryAddSolution(split);
|
||||
target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stomach was full or can't handle whatever solution we have.
|
||||
_contents.TryAddSolution(split);
|
||||
target.PopupMessage(Loc.GetString("You've had enough {0:theName}!", Owner));
|
||||
return false;
|
||||
if (_useSound != null)
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_useSound, target, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
|
||||
target.PopupMessage(Loc.GetString("Slurp"));
|
||||
UpdateAppearance();
|
||||
|
||||
// TODO: Account for partial transfer.
|
||||
|
||||
foreach (var (reagentId, quantity) in split.Contents)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype reagent)) continue;
|
||||
split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity));
|
||||
}
|
||||
|
||||
firstStomach.TryTransferSolution(split);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ILand.Land(LandEventArgs eventArgs)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Body.Digestive;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Utensil;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body.Behavior;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Utensil;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
@@ -129,7 +131,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
||||
|
||||
var trueTarget = target ?? user;
|
||||
|
||||
if (!trueTarget.TryGetComponent(out StomachComponent? stomach))
|
||||
if (!trueTarget.TryGetMechanismBehaviors<SharedStomachBehaviorComponent>(out var stomachs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -171,7 +173,9 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
||||
|
||||
var transferAmount = ReagentUnit.Min(_transferAmount, solution.CurrentVolume);
|
||||
var split = solution.SplitSolution(transferAmount);
|
||||
if (!stomach.CanTransferSolution(split))
|
||||
var firstStomach = stomachs.FirstOrDefault(stomach => stomach.CanTransferSolution(split));
|
||||
|
||||
if (firstStomach == null)
|
||||
{
|
||||
trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!"));
|
||||
return false;
|
||||
@@ -185,7 +189,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
||||
split.RemoveReagent(reagentId, reagent.ReactionEntity(target, ReactionMethod.Ingestion, quantity));
|
||||
}
|
||||
|
||||
stomach.TryTransferSolution(split);
|
||||
firstStomach.TryTransferSolution(split);
|
||||
|
||||
_entitySystem.GetEntitySystem<AudioSystem>()
|
||||
.PlayFromEntity(_useSound, trueTarget, AudioParams.Default.WithVolume(-1f));
|
||||
|
||||
@@ -189,7 +189,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
||||
{
|
||||
if (Owner.TryGetComponent(out IDamageableComponent damageable))
|
||||
{
|
||||
if (damageable.CurrentDamageState != DamageState.Dead)
|
||||
if (damageable.CurrentState != DamageState.Dead)
|
||||
{
|
||||
damageable.ChangeDamage(DamageType.Blunt, 2, true, null);
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
||||
{
|
||||
if (Owner.TryGetComponent(out IDamageableComponent damageable))
|
||||
{
|
||||
if (damageable.CurrentDamageState != DamageState.Dead)
|
||||
if (damageable.CurrentState != DamageState.Dead)
|
||||
{
|
||||
damageable.ChangeDamage(DamageType.Blunt, 2, true, null);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Content.Server.GameObjects.Components.Recycling
|
||||
|
||||
private bool CanGib(IEntity entity)
|
||||
{
|
||||
return entity.HasComponent<ISharedBodyManagerComponent>() && !_safe && Powered;
|
||||
return entity.HasComponent<IBody>() && !_safe && Powered;
|
||||
}
|
||||
|
||||
private bool CanRecycle(IEntity entity, [MaybeNullWhen(false)] out ConstructionPrototype prototype)
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Content.Server.GameObjects.Components.Suspicion
|
||||
public bool IsDead()
|
||||
{
|
||||
return Owner.TryGetComponent(out IDamageableComponent? damageable) &&
|
||||
damageable.CurrentDamageState == DamageState.Dead;
|
||||
damageable.CurrentState == DamageState.Dead;
|
||||
}
|
||||
|
||||
public bool IsInnocent()
|
||||
|
||||
Reference in New Issue
Block a user