Re-organize all projects (#4166)
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.AI
|
||||
{
|
||||
[Prototype("aiFaction")]
|
||||
public class AiFactionPrototype : IPrototype
|
||||
{
|
||||
// These are immutable so any dynamic changes aren't saved back over.
|
||||
// AiFactionSystem will just read these and then store them.
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("hostile")]
|
||||
public IReadOnlyList<string> Hostile { get; private set; } = new List<string>();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.EntitySystems.AI;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.AI
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class AiFactionTagComponent : Component
|
||||
{
|
||||
public override string Name => "AiFactionTag";
|
||||
|
||||
[DataField("factions")]
|
||||
public Faction Factions { get; private set; } = Faction.None;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Interfaces;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Access
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple mutable access provider found on ID cards and such.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IAccess))]
|
||||
public class AccessComponent : Component, IAccess
|
||||
{
|
||||
public override string Name => "Access";
|
||||
|
||||
[DataField("tags")]
|
||||
[ViewVariables]
|
||||
private readonly HashSet<string> _tags = new();
|
||||
|
||||
public ISet<string> Tags => _tags;
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void SetTags(IEnumerable<string> newTags)
|
||||
{
|
||||
_tags.Clear();
|
||||
_tags.UnionWith(newTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Access
|
||||
{
|
||||
public sealed class AccessReaderChangeMessage : EntityEventArgs
|
||||
{
|
||||
public IEntity Sender { get; }
|
||||
|
||||
public bool Enabled { get; }
|
||||
|
||||
public AccessReaderChangeMessage(IEntity entity, bool enabled)
|
||||
{
|
||||
Sender = entity;
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Access
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores access levels necessary to "use" an entity
|
||||
/// and allows checking if something or somebody is authorized with these access levels.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
[RegisterComponent]
|
||||
public class AccessReader : Component
|
||||
{
|
||||
public override string Name => "AccessReader";
|
||||
|
||||
private readonly HashSet<string> _denyTags = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of access lists to check allowed against. For an access check to pass
|
||||
/// there has to be an access list that is a subset of the access in the checking list.
|
||||
/// </summary>
|
||||
[DataField("access")]
|
||||
[ViewVariables]
|
||||
public List<HashSet<string>> AccessLists { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The set of tags that will automatically deny an allowed check, if any of them are present.
|
||||
/// </summary>
|
||||
[ViewVariables] public ISet<string> DenyTags => _denyTags;
|
||||
|
||||
/// <summary>
|
||||
/// Searches an <see cref="IAccess"/> in the entity itself, in its active hand or in its ID slot.
|
||||
/// Then compares the found access with the configured access lists to see if it is allowed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no access is found, an empty set is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="entity">The entity to be searched for access.</param>
|
||||
public bool IsAllowed(IEntity entity)
|
||||
{
|
||||
var tags = FindAccessTags(entity);
|
||||
return IsAllowed(tags);
|
||||
}
|
||||
|
||||
public bool IsAllowed(IAccess access)
|
||||
{
|
||||
return IsAllowed(access.Tags);
|
||||
}
|
||||
|
||||
public bool IsAllowed(ICollection<string> accessTags)
|
||||
{
|
||||
if (_denyTags.Overlaps(accessTags))
|
||||
{
|
||||
// Sec owned by cargo.
|
||||
return false;
|
||||
}
|
||||
|
||||
return AccessLists.Count == 0 || AccessLists.Any(a => a.IsSubsetOf(accessTags));
|
||||
}
|
||||
|
||||
public static ICollection<string> FindAccessTags(IEntity entity)
|
||||
{
|
||||
if (entity.TryGetComponent(out IAccess? accessComponent))
|
||||
{
|
||||
return accessComponent.Tags;
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out IHandsComponent? handsComponent))
|
||||
{
|
||||
var activeHandEntity = handsComponent.GetActiveHand?.Owner;
|
||||
if (activeHandEntity != null &&
|
||||
activeHandEntity.TryGetComponent(out IAccess? handAccessComponent))
|
||||
{
|
||||
return handAccessComponent.Tags;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
if (entity.TryGetComponent(out InventoryComponent? inventoryComponent))
|
||||
{
|
||||
if (inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) &&
|
||||
inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent? item) &&
|
||||
item.Owner.TryGetComponent(out IAccess? idAccessComponent)
|
||||
)
|
||||
{
|
||||
return idAccessComponent.Tags;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
var proto = IoCManager.Resolve<IPrototypeManager>();
|
||||
foreach (var level in AccessLists.SelectMany(c => c).Union(DenyTags))
|
||||
{
|
||||
if (!proto.HasIndex<AccessLevelPrototype>(level))
|
||||
{
|
||||
Logger.ErrorS("access", $"Invalid access level: {level}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Access
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class IdCardComponent : Component
|
||||
{
|
||||
public override string Name => "IdCard";
|
||||
|
||||
/// See <see cref="UpdateEntityName"/>.
|
||||
[DataField("originalOwnerName")]
|
||||
private string _originalOwnerName = default!;
|
||||
|
||||
[DataField("fullName")]
|
||||
private string? _fullName;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? FullName
|
||||
{
|
||||
get => _fullName;
|
||||
set
|
||||
{
|
||||
_fullName = value;
|
||||
UpdateEntityName();
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("jobTitle")]
|
||||
private string? _jobTitle;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? JobTitle
|
||||
{
|
||||
get => _jobTitle;
|
||||
set
|
||||
{
|
||||
_jobTitle = value;
|
||||
UpdateEntityName();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the <see cref="Entity.Name"/> of <see cref="Component.Owner"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If either <see cref="FullName"/> or <see cref="JobTitle"/> is empty, it's replaced by placeholders.
|
||||
/// If both are empty, the original entity's name is restored.
|
||||
/// </remarks>
|
||||
private void UpdateEntityName()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(FullName) && string.IsNullOrWhiteSpace(JobTitle))
|
||||
{
|
||||
Owner.Name = _originalOwnerName;
|
||||
return;
|
||||
}
|
||||
|
||||
var jobSuffix = string.IsNullOrWhiteSpace(JobTitle) ? "" : $" ({JobTitle})";
|
||||
|
||||
Owner.Name = string.IsNullOrWhiteSpace(FullName)
|
||||
? Loc.GetString("{0}{1}", _originalOwnerName, jobSuffix)
|
||||
: Loc.GetString("{0}'s ID card{1}", FullName, jobSuffix);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
// ReSharper disable once ConstantNullCoalescingCondition
|
||||
_originalOwnerName ??= Owner.Name;
|
||||
UpdateEntityName();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.GameObjects.Components.Access;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Access
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
public class IdCardConsoleComponent : SharedIdCardConsoleComponent, IActivate, IInteractUsing, IBreakAct
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private ContainerSlot _privilegedIdContainer = default!;
|
||||
private ContainerSlot _targetIdContainer = default!;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(IdCardConsoleUiKey.Key);
|
||||
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
|
||||
|
||||
private bool PrivilegedIDEmpty => _privilegedIdContainer.ContainedEntities.Count < 1;
|
||||
private bool TargetIDEmpty => _targetIdContainer.ContainedEntities.Count < 1;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_privilegedIdContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-privilegedId");
|
||||
_targetIdContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-targetId");
|
||||
|
||||
Owner.EnsureComponentWarn<AccessReader>();
|
||||
Owner.EnsureComponentWarn<ServerUserInterfaceComponent>();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
|
||||
}
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
if (obj.Session.AttachedEntity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (obj.Message)
|
||||
{
|
||||
case IdButtonPressedMessage msg:
|
||||
switch (msg.Button)
|
||||
{
|
||||
case UiButton.PrivilegedId:
|
||||
HandleId(obj.Session.AttachedEntity, _privilegedIdContainer);
|
||||
break;
|
||||
case UiButton.TargetId:
|
||||
HandleId(obj.Session.AttachedEntity, _targetIdContainer);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case WriteToTargetIdMessage msg:
|
||||
TryWriteToTargetId(msg.FullName, msg.JobTitle, msg.AccessList);
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if there is an ID in <see cref="_privilegedIdContainer"/> and said ID satisfies the requirements of <see cref="AccessReader"/>.
|
||||
/// </summary>
|
||||
private bool PrivilegedIdIsAuthorized()
|
||||
{
|
||||
if (!Owner.TryGetComponent(out AccessReader? reader))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var privilegedIdEntity = _privilegedIdContainer.ContainedEntity;
|
||||
return privilegedIdEntity != null && reader.IsAllowed(privilegedIdEntity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the "Submit" button in the UI gets pressed.
|
||||
/// Writes data passed from the UI into the ID stored in <see cref="_targetIdContainer"/>, if present.
|
||||
/// </summary>
|
||||
private void TryWriteToTargetId(string newFullName, string newJobTitle, List<string> newAccessList)
|
||||
{
|
||||
if (!PrivilegedIdIsAuthorized() || _targetIdContainer.ContainedEntity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetIdEntity = _targetIdContainer.ContainedEntity;
|
||||
|
||||
var targetIdComponent = targetIdEntity.GetComponent<IdCardComponent>();
|
||||
targetIdComponent.FullName = newFullName;
|
||||
targetIdComponent.JobTitle = newJobTitle;
|
||||
|
||||
if (!newAccessList.TrueForAll(x => _prototypeManager.HasIndex<AccessLevelPrototype>(x)))
|
||||
{
|
||||
Logger.Warning("Tried to write unknown access tag.");
|
||||
return;
|
||||
}
|
||||
var targetIdAccess = targetIdEntity.GetComponent<AccessComponent>();
|
||||
targetIdAccess.SetTags(newAccessList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when one of the insert/remove ID buttons gets pressed.
|
||||
/// </summary>
|
||||
private void HandleId(IEntity user, ContainerSlot container)
|
||||
{
|
||||
if (!user.TryGetComponent(out IHandsComponent? hands))
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("You have no hands."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (container.ContainedEntity == null)
|
||||
{
|
||||
InsertIdFromHand(user, container, hands);
|
||||
}
|
||||
else
|
||||
{
|
||||
PutIdInHand(container, hands);
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertIdFromHand(IEntity user, ContainerSlot container, IHandsComponent hands)
|
||||
{
|
||||
var isId = hands.GetActiveHand?.Owner.HasComponent<IdCardComponent>();
|
||||
if (isId != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (hands.ActiveHand == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hands.Drop(hands.ActiveHand, container))
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("You can't let go of the ID card!"));
|
||||
return;
|
||||
}
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
private void PutIdInHand(ContainerSlot container, IHandsComponent hands)
|
||||
{
|
||||
var idEntity = container.ContainedEntity;
|
||||
if (idEntity == null || !container.Remove(idEntity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
UpdateUserInterface();
|
||||
|
||||
hands.PutInHand(idEntity.GetComponent<ItemComponent>());
|
||||
}
|
||||
|
||||
private void UpdateUserInterface()
|
||||
{
|
||||
var isPrivilegedIdPresent = _privilegedIdContainer.ContainedEntity != null;
|
||||
var targetIdEntity = _targetIdContainer.ContainedEntity;
|
||||
IdCardConsoleBoundUserInterfaceState newState;
|
||||
// this could be prettier
|
||||
if (targetIdEntity == null)
|
||||
{
|
||||
newState = new IdCardConsoleBoundUserInterfaceState(
|
||||
isPrivilegedIdPresent,
|
||||
PrivilegedIdIsAuthorized(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
_privilegedIdContainer.ContainedEntity?.Name ?? "",
|
||||
_targetIdContainer.ContainedEntity?.Name ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetIdComponent = targetIdEntity.GetComponent<IdCardComponent>();
|
||||
var targetAccessComponent = targetIdEntity.GetComponent<AccessComponent>();
|
||||
newState = new IdCardConsoleBoundUserInterfaceState(
|
||||
isPrivilegedIdPresent,
|
||||
PrivilegedIdIsAuthorized(),
|
||||
true,
|
||||
targetIdComponent.FullName,
|
||||
targetIdComponent.JobTitle,
|
||||
targetAccessComponent.Tags.ToArray(),
|
||||
_privilegedIdContainer.ContainedEntity?.Name ?? "",
|
||||
_targetIdContainer.ContainedEntity?.Name ?? "");
|
||||
}
|
||||
UserInterface?.SetState(newState);
|
||||
}
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if(!eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(!Powered) return;
|
||||
|
||||
UserInterface?.Open(actor.PlayerSession);
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
var item = eventArgs.Using;
|
||||
var user = eventArgs.User;
|
||||
|
||||
if (!PrivilegedIDEmpty && !TargetIDEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!item.TryGetComponent<IdCardComponent>(out var idCardComponent) || !user.TryGetComponent(out IHandsComponent? hand))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PrivilegedIDEmpty)
|
||||
{
|
||||
InsertIdFromHand(user, _privilegedIdContainer, hand);
|
||||
}
|
||||
|
||||
else if (TargetIDEmpty)
|
||||
{
|
||||
InsertIdFromHand(user, _targetIdContainer, hand);
|
||||
}
|
||||
|
||||
UpdateUserInterface();
|
||||
return true;
|
||||
}
|
||||
|
||||
[Verb]
|
||||
public sealed class EjectPrivilegedIDVerb : Verb<IdCardConsoleComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Eject Privileged ID");
|
||||
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
|
||||
data.Visibility = component.PrivilegedIDEmpty ? VerbVisibility.Invisible : VerbVisibility.Visible;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, IdCardConsoleComponent component)
|
||||
{
|
||||
if (!user.TryGetComponent(out IHandsComponent? hand))
|
||||
{
|
||||
return;
|
||||
}
|
||||
component.PutIdInHand(component._privilegedIdContainer, hand);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EjectTargetIDVerb : Verb<IdCardConsoleComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, IdCardConsoleComponent component, VerbData data)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Eject Target ID");
|
||||
data.Visibility = component.TargetIDEmpty ? VerbVisibility.Invisible : VerbVisibility.Visible;
|
||||
data.IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png";
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, IdCardConsoleComponent component)
|
||||
{
|
||||
if (!user.TryGetComponent(out IHandsComponent? hand))
|
||||
{
|
||||
return;
|
||||
}
|
||||
component.PutIdInHand(component._targetIdContainer, hand);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBreak(BreakageEventArgs eventArgs)
|
||||
{
|
||||
var privileged = _privilegedIdContainer.ContainedEntity;
|
||||
if (privileged != null)
|
||||
_privilegedIdContainer.Remove(privileged);
|
||||
|
||||
var target = _targetIdContainer.ContainedEntity;
|
||||
if (target != null)
|
||||
_targetIdContainer.Remove(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Access
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class PresetIdCardComponent : Component, IMapInit
|
||||
{
|
||||
public override string Name => "PresetIdCard";
|
||||
|
||||
[DataField("job")]
|
||||
private string? _jobName;
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_jobName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var prototypes = IoCManager.Resolve<IPrototypeManager>();
|
||||
var job = prototypes.Index<JobPrototype>(_jobName);
|
||||
var access = Owner.GetComponent<AccessComponent>();
|
||||
var idCard = Owner.GetComponent<IdCardComponent>();
|
||||
|
||||
access.SetTags(job.Access);
|
||||
idCard.JobTitle = job.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CuffableComponent : SharedCuffableComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// How many of this entity's hands are currently cuffed.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public int CuffedHandCount => Container.ContainedEntities.Count * 2;
|
||||
|
||||
protected IEntity LastAddedCuffs => Container.ContainedEntities[^1];
|
||||
|
||||
public IReadOnlyList<IEntity> StoredEntities => Container.ContainedEntities;
|
||||
|
||||
/// <summary>
|
||||
/// Container of various handcuffs currently applied to the entity.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public Container Container { get; set; } = default!;
|
||||
|
||||
// TODO: Make a component message
|
||||
public event Action? OnCuffedStateChanged;
|
||||
|
||||
private bool _uncuffing;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Container = ContainerHelpers.EnsureContainer<Container>(Owner, Name);
|
||||
Owner.EnsureComponentWarn<HandsComponent>();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
// there are 2 approaches i can think of to handle the handcuff overlay on players
|
||||
// 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
|
||||
// 2 - allow for several different player overlays for each different cuff type.
|
||||
// approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
|
||||
// right now we're doing approach #1.
|
||||
|
||||
if (CuffedHandCount > 0)
|
||||
{
|
||||
if (LastAddedCuffs.TryGetComponent<HandcuffComponent>(out var cuffs))
|
||||
{
|
||||
return new CuffableComponentState(CuffedHandCount,
|
||||
CanStillInteract,
|
||||
cuffs.CuffedRSI,
|
||||
$"{cuffs.OverlayIconState}-{CuffedHandCount}",
|
||||
cuffs.Color);
|
||||
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
|
||||
// the number corresponds to how many hands are cuffed.
|
||||
}
|
||||
}
|
||||
|
||||
return new CuffableComponentState(CuffedHandCount,
|
||||
CanStillInteract,
|
||||
"/Objects/Misc/handcuffs.rsi",
|
||||
"body-overlay-2",
|
||||
Color.White);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a set of cuffs to an existing CuffedComponent.
|
||||
/// </summary>
|
||||
/// <param name="prototype"></param>
|
||||
public bool TryAddNewCuffs(IEntity user, IEntity handcuff)
|
||||
{
|
||||
if (!handcuff.HasComponent<HandcuffComponent>())
|
||||
{
|
||||
Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!handcuff.InRangeUnobstructed(Owner))
|
||||
{
|
||||
Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Success!
|
||||
if (user.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.IsHolding(handcuff))
|
||||
{
|
||||
// Good lord handscomponent is scuffed, I hope some smug person will fix it someday
|
||||
handsComponent.Drop(handcuff);
|
||||
}
|
||||
|
||||
Container.Insert(handcuff);
|
||||
CanStillInteract = Owner.TryGetComponent(out HandsComponent? ownerHands) && ownerHands.Hands.Count() > CuffedHandCount;
|
||||
|
||||
OnCuffedStateChanged?.Invoke();
|
||||
UpdateAlert();
|
||||
UpdateHeldItems();
|
||||
Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CuffedStateChanged()
|
||||
{
|
||||
UpdateAlert();
|
||||
OnCuffedStateChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check how many items the user is holding and if it's more than the number of cuffed hands, drop some items.
|
||||
/// </summary>
|
||||
public void UpdateHeldItems()
|
||||
{
|
||||
if (!Owner.TryGetComponent(out HandsComponent? handsComponent)) return;
|
||||
|
||||
var itemCount = handsComponent.GetAllHeldItems().Count();
|
||||
var freeHandCount = handsComponent.Hands.Count() - CuffedHandCount;
|
||||
|
||||
if (freeHandCount < itemCount)
|
||||
{
|
||||
foreach (var item in handsComponent.GetAllHeldItems())
|
||||
{
|
||||
if (freeHandCount < itemCount)
|
||||
{
|
||||
freeHandCount++;
|
||||
handsComponent.Drop(item.Owner, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the status effect indicator on the HUD.
|
||||
/// </summary>
|
||||
private void UpdateAlert()
|
||||
{
|
||||
if (Owner.TryGetComponent(out ServerAlertsComponent? status))
|
||||
{
|
||||
if (CanStillInteract)
|
||||
{
|
||||
status.ClearAlert(AlertType.Handcuffed);
|
||||
}
|
||||
else
|
||||
{
|
||||
status.ShowAlert(AlertType.Handcuffed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
|
||||
/// If the uncuffing succeeds, the cuffs will drop on the floor.
|
||||
/// </summary>
|
||||
/// <param name="user">The cuffed entity</param>
|
||||
/// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
|
||||
public async void TryUncuff(IEntity user, IEntity? cuffsToRemove = null)
|
||||
{
|
||||
if (_uncuffing) return;
|
||||
|
||||
var isOwner = user == Owner;
|
||||
|
||||
if (cuffsToRemove == null)
|
||||
{
|
||||
if (Container.ContainedEntities.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cuffsToRemove = LastAddedCuffs;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Container.ContainedEntities.Contains(cuffsToRemove))
|
||||
{
|
||||
Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
|
||||
}
|
||||
}
|
||||
|
||||
if (!cuffsToRemove.TryGetComponent<HandcuffComponent>(out var cuff))
|
||||
{
|
||||
Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You can't do that!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwner && !user.InRangeUnobstructed(Owner))
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You are too far away to remove the cuffs."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cuffsToRemove.InRangeUnobstructed(Owner))
|
||||
{
|
||||
Logger.Warning("Handcuffs being removed from player are obstructed or too far away! This should not happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
user.PopupMessage(Loc.GetString("You start removing the cuffs."));
|
||||
|
||||
var audio = EntitySystem.Get<AudioSystem>();
|
||||
if (isOwner)
|
||||
{
|
||||
if (cuff.StartBreakoutSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(Owner), cuff.StartBreakoutSound, Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cuff.StartUncuffSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(Owner), cuff.StartUncuffSound, Owner);
|
||||
}
|
||||
|
||||
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
|
||||
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
||||
_uncuffing = true;
|
||||
|
||||
var result = await doAfterSystem.DoAfter(doAfterEventArgs);
|
||||
|
||||
_uncuffing = false;
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
if (cuff.EndUncuffSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(Owner), cuff.EndUncuffSound, Owner);
|
||||
|
||||
Container.ForceRemove(cuffsToRemove);
|
||||
cuffsToRemove.Transform.AttachToGridOrMap();
|
||||
cuffsToRemove.Transform.WorldPosition = Owner.Transform.WorldPosition;
|
||||
|
||||
if (cuff.BreakOnRemove)
|
||||
{
|
||||
cuff.Broken = true;
|
||||
|
||||
cuffsToRemove.Name = cuff.BrokenName;
|
||||
cuffsToRemove.Description = cuff.BrokenDesc;
|
||||
|
||||
if (cuffsToRemove.TryGetComponent<SpriteComponent>(out var sprite) && cuff.BrokenState != null)
|
||||
{
|
||||
sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state?
|
||||
}
|
||||
}
|
||||
|
||||
CanStillInteract = Owner.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.Hands.Count() > CuffedHandCount;
|
||||
OnCuffedStateChanged?.Invoke();
|
||||
UpdateAlert();
|
||||
Dirty();
|
||||
|
||||
if (CuffedHandCount == 0)
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You successfully remove the cuffs."));
|
||||
|
||||
if (!isOwner)
|
||||
{
|
||||
user.PopupMessage(Owner, Loc.GetString("{0:theName} uncuffs your hands.", user));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isOwner)
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You successfully remove the cuffs. {0} of {1:theName}'s hands remain cuffed.", CuffedHandCount, user));
|
||||
user.PopupMessage(Owner, Loc.GetString("{0:theName} removes your cuffs. {1} of your hands remain cuffed.", user, CuffedHandCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You successfully remove the cuffs. {0} of your hands remain cuffed.", CuffedHandCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You fail to remove the cuffs."));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the uncuffing of a cuffed person. Used by other people and by the component owner to break out of cuffs.
|
||||
/// </summary>
|
||||
[Verb]
|
||||
private sealed class UncuffVerb : Verb<CuffableComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, CuffableComponent component, VerbData data)
|
||||
{
|
||||
if ((user != component.Owner && !ActionBlockerSystem.CanInteract(user)) || component.CuffedHandCount == 0)
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Uncuff");
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, CuffableComponent component)
|
||||
{
|
||||
if (component.CuffedHandCount > 0)
|
||||
{
|
||||
component.TryUncuff(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedHandcuffComponent))]
|
||||
public class HandcuffComponent : SharedHandcuffComponent, IAfterInteract
|
||||
{
|
||||
/// <summary>
|
||||
/// The time it takes to apply a <see cref="CuffedComponent"/> to an entity.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("cuffTime")]
|
||||
public float CuffTime { get; set; } = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to remove a <see cref="CuffedComponent"/> from an entity.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("uncuffTime")]
|
||||
public float UncuffTime { get; set; } = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes for a cuffed entity to remove <see cref="CuffedComponent"/> from itself.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("breakoutTime")]
|
||||
public float BreakoutTime { get; set; } = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("stunBonus")]
|
||||
public float StunBonus { get; set; } = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// Will the cuffs break when removed?
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("breakOnRemove")]
|
||||
public bool BreakOnRemove { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the RSI file used for the player cuffed overlay.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("cuffedRSI")]
|
||||
public string? CuffedRSI { get; set; } = "Objects/Misc/handcuffs.rsi";
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used with the RSI file for the player cuffed overlay.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("bodyIconState")]
|
||||
public string? OverlayIconState { get; set; } = "body-overlay";
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("brokenIconState")]
|
||||
public string? BrokenState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("brokenName")]
|
||||
public string BrokenName { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("brokenDesc")]
|
||||
public string BrokenDesc { get; set; } = default!;
|
||||
|
||||
[ViewVariables]
|
||||
public bool Broken
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isBroken;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_isBroken != value)
|
||||
{
|
||||
_isBroken = value;
|
||||
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("startCuffSound")]
|
||||
public string StartCuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_start.ogg";
|
||||
|
||||
[DataField("endCuffSound")] public string EndCuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_end.ogg";
|
||||
|
||||
[DataField("startBreakoutSound")]
|
||||
public string StartBreakoutSound { get; set; } = "/Audio/Items/Handcuffs/cuff_breakout_start.ogg";
|
||||
|
||||
[DataField("startUncuffSound")]
|
||||
public string StartUncuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_takeoff_start.ogg";
|
||||
|
||||
[DataField("endUncuffSound")]
|
||||
public string EndUncuffSound { get; set; } = "/Audio/Items/Handcuffs/cuff_takeoff_end.ogg";
|
||||
[DataField("color")]
|
||||
public Color Color { get; set; } = Color.White;
|
||||
|
||||
// Non-exposed data fields
|
||||
private bool _isBroken = false;
|
||||
|
||||
/// <summary>
|
||||
/// Used to prevent DoAfter getting spammed.
|
||||
/// </summary>
|
||||
private bool _cuffing;
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
return new HandcuffedComponentState(Broken ? BrokenState : string.Empty);
|
||||
}
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (_cuffing) return true;
|
||||
|
||||
if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent<CuffableComponent>(out var cuffed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eventArgs.Target == eventArgs.User)
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("You can't cuff yourself!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Broken)
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("The cuffs are broken!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!eventArgs.Target.TryGetComponent<HandsComponent>(out var hands))
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no hands!", eventArgs.Target));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cuffed.CuffedHandCount == hands.Count)
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true))
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("You are too far away to use the cuffs!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
eventArgs.User.PopupMessage(Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target));
|
||||
eventArgs.User.PopupMessage(eventArgs.Target, Loc.GetString("{0:theName} starts cuffing you!", eventArgs.User));
|
||||
|
||||
if (StartCuffSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(Owner), StartCuffSound, Owner);
|
||||
|
||||
TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the cuffed state of an entity
|
||||
/// </summary>
|
||||
private async void TryUpdateCuff(IEntity user, IEntity target, CuffableComponent cuffs)
|
||||
{
|
||||
var cuffTime = CuffTime;
|
||||
|
||||
if (target.TryGetComponent<StunnableComponent>(out var stun) && stun.Stunned)
|
||||
{
|
||||
cuffTime = MathF.Max(0.1f, cuffTime - StunBonus);
|
||||
}
|
||||
|
||||
var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target)
|
||||
{
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
_cuffing = true;
|
||||
|
||||
var result = await EntitySystem.Get<DoAfterSystem>().DoAfter(doAfterEventArgs);
|
||||
|
||||
_cuffing = false;
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
if (cuffs.TryAddNewCuffs(user, Owner))
|
||||
{
|
||||
if (EndCuffSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(Owner), EndCuffSound, Owner);
|
||||
|
||||
user.PopupMessage(Loc.GetString("You successfully cuff {0:theName}.", target));
|
||||
target.PopupMessage(Loc.GetString("You have been cuffed by {0:theName}!", user));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You were interrupted while cuffing {0:theName}!", target));
|
||||
target.PopupMessage(Loc.GetString("You interrupt {0:theName} while they are cuffing you!", user));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.Mobs.Roles;
|
||||
using Content.Shared.GameObjects.Components.Actor;
|
||||
using Content.Shared.Objectives;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Actor
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CharacterInfoComponent : SharedCharacterInfoComponent
|
||||
{
|
||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null)
|
||||
{
|
||||
if(session?.AttachedEntity != Owner) return;
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case RequestCharacterInfoMessage _:
|
||||
var conditions = new Dictionary<string, List<ConditionInfo>>();
|
||||
var jobTitle = "No Profession";
|
||||
if (Owner.TryGetComponent(out MindComponent? mindComponent))
|
||||
{
|
||||
var mind = mindComponent.Mind;
|
||||
|
||||
if (mind != null)
|
||||
{
|
||||
// getting conditions
|
||||
foreach (var objective in mind.AllObjectives)
|
||||
{
|
||||
if (!conditions.ContainsKey(objective.Prototype.Issuer))
|
||||
conditions[objective.Prototype.Issuer] = new List<ConditionInfo>();
|
||||
foreach (var condition in objective.Conditions)
|
||||
{
|
||||
conditions[objective.Prototype.Issuer].Add(new ConditionInfo(condition.Title,
|
||||
condition.Description, condition.Icon, condition.Progress));
|
||||
}
|
||||
}
|
||||
|
||||
// getting jobtitle
|
||||
foreach (var role in mind.AllRoles)
|
||||
{
|
||||
if (role.GetType() == typeof(Job))
|
||||
{
|
||||
jobTitle = role.Name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SendNetworkMessage(new CharacterInfoMessage(jobTitle, conditions));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Content.Server.Advertisements;
|
||||
using Content.Server.Interfaces.Chat;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class AdvertiseComponent : Component
|
||||
{
|
||||
public override string Name => "Advertise";
|
||||
|
||||
private CancellationTokenSource _cancellationSource = new();
|
||||
|
||||
/// <summary>
|
||||
/// Minimum time to wait before saying a new ad, in seconds. Has to be larger than or equal to 1.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("minWait")]
|
||||
private int MinWait { get; } = 480; // 8 minutes
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time to wait before saying a new ad, in seconds. Has to be larger than or equal
|
||||
/// to <see cref="MinWait"/>
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("maxWait")]
|
||||
private int MaxWait { get; } = 600; // 10 minutes
|
||||
|
||||
[DataField("pack")]
|
||||
private string PackPrototypeId { get; } = string.Empty;
|
||||
|
||||
private List<string> _advertisements = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
IoCManager.Resolve<IPrototypeManager>().TryIndex(PackPrototypeId, out AdvertisementsPackPrototype? packPrototype);
|
||||
|
||||
// Load advertisements pack
|
||||
if (string.IsNullOrEmpty(PackPrototypeId) || packPrototype == null)
|
||||
{
|
||||
// If there is no pack, log a warning and don't start timer
|
||||
Logger.Warning($"{Owner} has {Name}Component but no advertisments pack.");
|
||||
return;
|
||||
}
|
||||
|
||||
_advertisements = packPrototype.Advertisements;
|
||||
|
||||
// Do not start timer if advertisement list is empty
|
||||
if (_advertisements.Count == 0)
|
||||
{
|
||||
// If no advertisements could be loaded, log a warning and don't start timer
|
||||
Logger.Warning($"{Owner} tried to load advertisements pack without ads.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Throw exception if MinWait is smaller than 1.
|
||||
if (MinWait < 1)
|
||||
{
|
||||
throw new PrototypeLoadException($"{Owner} has illegal minWait for {Name}Component: {MinWait}.");
|
||||
}
|
||||
|
||||
// Throw exception if MinWait larger than MaxWait.
|
||||
if (MinWait > MaxWait)
|
||||
{
|
||||
throw new PrototypeLoadException($"{Owner} should have minWait greater than or equal to maxWait for {Name}Component.");
|
||||
}
|
||||
|
||||
// Start timer at initialization, without MinWait bound
|
||||
RefreshTimer(false);
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
_cancellationSource.Cancel();
|
||||
_cancellationSource.Dispose();
|
||||
|
||||
base.OnRemove();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Say advertisement and restart timer.
|
||||
/// </summary>
|
||||
private void SayAndRefresh()
|
||||
{
|
||||
IRobustRandom random = IoCManager.Resolve<IRobustRandom>();
|
||||
IChatManager chatManager = IoCManager.Resolve<IChatManager>();
|
||||
|
||||
// Say advertisement
|
||||
chatManager.EntitySay(Owner, Loc.GetString(random.Pick(_advertisements)));
|
||||
|
||||
// Refresh timer to repeat cycle
|
||||
RefreshTimer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh cancellation token and spawn new timer with random wait between <see cref="MinWait"/>
|
||||
/// and <see cref="MaxWait"/>.
|
||||
/// <param name="minBound">
|
||||
/// Whether <see cref="MinWait"/> should be used to have a minimum waiting time.
|
||||
/// </param>
|
||||
/// </summary>
|
||||
private void RefreshTimer(bool minBound = true)
|
||||
{
|
||||
// Generate new source
|
||||
_cancellationSource.Cancel();
|
||||
_cancellationSource.Dispose();
|
||||
_cancellationSource = new CancellationTokenSource();
|
||||
|
||||
// Generate random wait time, then create timer
|
||||
IRobustRandom random = IoCManager.Resolve<IRobustRandom>();
|
||||
var wait = minBound ? random.Next(MinWait * 1000, MaxWait * 1000) : random.Next(MaxWait * 1000);
|
||||
Owner.SpawnTimer(wait, SayAndRefresh, _cancellationSource.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pause the advertising until <see cref="Resume"/> is called.
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
// Cancel current timer
|
||||
_cancellationSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resume the advertising after pausing.
|
||||
/// </summary>
|
||||
public void Resume()
|
||||
{
|
||||
// Restart timer, without minBound
|
||||
RefreshTimer(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Server.GameObjects.Components.Pulling;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Interactable;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components
|
||||
{
|
||||
// TODO: Move this component's logic to an EntitySystem.
|
||||
[RegisterComponent]
|
||||
public class AnchorableComponent : Component, IInteractUsing
|
||||
{
|
||||
public override string Name => "Anchorable";
|
||||
|
||||
[ComponentDependency] private PhysicsComponent? _physicsComponent = default!;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("tool")]
|
||||
public ToolQuality Tool { get; private set; } = ToolQuality.Anchoring;
|
||||
|
||||
[ViewVariables]
|
||||
int IInteractUsing.Priority => 1;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("snap")]
|
||||
public bool Snap { get; private set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a tool can change the anchored status.
|
||||
/// </summary>
|
||||
/// <param name="user">The user doing the action</param>
|
||||
/// <param name="utilizing">The tool being used</param>
|
||||
/// <param name="anchoring">True if we're anchoring, and false if we're unanchoring.</param>
|
||||
/// <returns>true if it is valid, false otherwise</returns>
|
||||
private async Task<bool> Valid(IEntity user, IEntity utilizing, bool anchoring)
|
||||
{
|
||||
if (!Owner.HasComponent<IPhysBody>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BaseAnchoredAttemptEvent attempt =
|
||||
anchoring ? new AnchorAttemptEvent(user, utilizing) : new UnanchorAttemptEvent(user, utilizing);
|
||||
|
||||
// Need to cast the event or it will be raised as BaseAnchoredAttemptEvent.
|
||||
if (anchoring)
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, (AnchorAttemptEvent) attempt, false);
|
||||
else
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, (UnanchorAttemptEvent) attempt, false);
|
||||
|
||||
if (attempt.Cancelled)
|
||||
return false;
|
||||
|
||||
return utilizing.TryGetComponent(out ToolComponent? tool) && await tool.UseTool(user, Owner, 0.5f + attempt.Delay, Tool);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to anchor the owner of this component.
|
||||
/// </summary>
|
||||
/// <param name="user">The entity doing the anchoring</param>
|
||||
/// <param name="utilizing">The tool being used</param>
|
||||
/// <returns>true if anchored, false otherwise</returns>
|
||||
public async Task<bool> TryAnchor(IEntity user, IEntity utilizing)
|
||||
{
|
||||
if (!(await Valid(user, utilizing, true)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_physicsComponent == null)
|
||||
return false;
|
||||
|
||||
// Snap rotation to cardinal (multiple of 90)
|
||||
var rot = Owner.Transform.LocalRotation;
|
||||
Owner.Transform.LocalRotation = Math.Round(rot / (Math.PI / 2)) * (Math.PI / 2);
|
||||
|
||||
if (Owner.TryGetComponent(out PullableComponent? pullableComponent))
|
||||
{
|
||||
if (pullableComponent.Puller != null)
|
||||
{
|
||||
pullableComponent.TryStopPull();
|
||||
}
|
||||
}
|
||||
|
||||
if (Snap)
|
||||
Owner.SnapToGrid(Owner.EntityManager);
|
||||
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new BeforeAnchoredEvent(user, utilizing), false);
|
||||
|
||||
_physicsComponent.BodyType = BodyType.Static;
|
||||
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new AnchoredEvent(user, utilizing), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to unanchor the owner of this component.
|
||||
/// </summary>
|
||||
/// <param name="user">The entity doing the unanchoring</param>
|
||||
/// <param name="utilizing">The tool being used, if any</param>
|
||||
/// <param name="force">Whether or not to ignore valid tool checks</param>
|
||||
/// <returns>true if unanchored, false otherwise</returns>
|
||||
public async Task<bool> TryUnAnchor(IEntity user, IEntity utilizing)
|
||||
{
|
||||
if (!(await Valid(user, utilizing, false)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_physicsComponent == null)
|
||||
return false;
|
||||
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new BeforeUnanchoredEvent(user, utilizing), false);
|
||||
|
||||
_physicsComponent.BodyType = BodyType.Dynamic;
|
||||
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(Owner.Uid, new UnanchoredEvent(user, utilizing), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to toggle the anchored status of this component's owner.
|
||||
/// </summary>
|
||||
/// <param name="user">The entity doing the unanchoring</param>
|
||||
/// <param name="utilizing">The tool being used</param>
|
||||
/// <returns>true if toggled, false otherwise</returns>
|
||||
public async Task<bool> TryToggleAnchor(IEntity user, IEntity utilizing)
|
||||
{
|
||||
if (_physicsComponent == null)
|
||||
return false;
|
||||
|
||||
return _physicsComponent.BodyType == BodyType.Static ?
|
||||
await TryUnAnchor(user, utilizing) :
|
||||
await TryAnchor(user, utilizing);
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
return await TryToggleAnchor(eventArgs.User, eventArgs.Using);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseAnchoredAttemptEvent : CancellableEntityEventArgs
|
||||
{
|
||||
public IEntity User { get; }
|
||||
public IEntity Tool { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Extra delay to add to the do_after.
|
||||
/// Add to this, don't replace it.
|
||||
/// Output parameter.
|
||||
/// </summary>
|
||||
public float Delay { get; set; } = 0f;
|
||||
|
||||
protected BaseAnchoredAttemptEvent(IEntity user, IEntity tool)
|
||||
{
|
||||
User = user;
|
||||
Tool = tool;
|
||||
}
|
||||
}
|
||||
|
||||
public class AnchorAttemptEvent : BaseAnchoredAttemptEvent
|
||||
{
|
||||
public AnchorAttemptEvent(IEntity user, IEntity tool) : base(user, tool) { }
|
||||
}
|
||||
|
||||
public class UnanchorAttemptEvent : BaseAnchoredAttemptEvent
|
||||
{
|
||||
public UnanchorAttemptEvent(IEntity user, IEntity tool) : base(user, tool) { }
|
||||
}
|
||||
|
||||
public abstract class BaseAnchoredEvent : EntityEventArgs
|
||||
{
|
||||
public IEntity User { get; }
|
||||
public IEntity Tool { get; }
|
||||
|
||||
protected BaseAnchoredEvent(IEntity user, IEntity tool)
|
||||
{
|
||||
User = user;
|
||||
Tool = tool;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised just before the entity's body type is changed.
|
||||
/// </summary>
|
||||
public class BeforeAnchoredEvent : BaseAnchoredEvent
|
||||
{
|
||||
public BeforeAnchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
|
||||
}
|
||||
|
||||
public class AnchoredEvent : BaseAnchoredEvent
|
||||
{
|
||||
public AnchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised just before the entity's body type is changed.
|
||||
/// </summary>
|
||||
public class BeforeUnanchoredEvent : BaseAnchoredEvent
|
||||
{
|
||||
public BeforeUnanchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
|
||||
}
|
||||
|
||||
public class UnanchoredEvent : BaseAnchoredEvent
|
||||
{
|
||||
public UnanchoredEvent(IEntity user, IEntity tool) : base(user, tool) { }
|
||||
}
|
||||
}
|
||||
@@ -1,899 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Arcade;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Arcade
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
public class BlockGameArcadeComponent : Component, IActivate
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override string Name => "BlockGameArcade";
|
||||
public override uint? NetID => ContentNetIDs.BLOCKGAME_ARCADE;
|
||||
|
||||
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
|
||||
|
||||
private bool Powered => _powerReceiverComponent?.Powered ?? false;
|
||||
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);
|
||||
|
||||
private BlockGame? _game;
|
||||
|
||||
private IPlayerSession? _player;
|
||||
private readonly List<IPlayerSession> _spectators = new();
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
OnPowerStateChanged(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if(!eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!Powered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(!ActionBlockerSystem.CanInteract(actor.PlayerSession.AttachedEntity)) return;
|
||||
|
||||
UserInterface?.Toggle(actor.PlayerSession);
|
||||
RegisterPlayerSession(actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void RegisterPlayerSession(IPlayerSession session)
|
||||
{
|
||||
if (_player == null) _player = session;
|
||||
else _spectators.Add(session);
|
||||
|
||||
UpdatePlayerStatus(session);
|
||||
_game?.UpdateNewPlayerUI(session);
|
||||
}
|
||||
|
||||
private void DeactivePlayer(IPlayerSession session)
|
||||
{
|
||||
if (_player != session) return;
|
||||
|
||||
var temp = _player;
|
||||
_player = null;
|
||||
if (_spectators.Count != 0)
|
||||
{
|
||||
_player = _spectators[0];
|
||||
_spectators.Remove(_player);
|
||||
UpdatePlayerStatus(_player);
|
||||
}
|
||||
_spectators.Add(temp);
|
||||
|
||||
UpdatePlayerStatus(temp);
|
||||
}
|
||||
|
||||
private void UnRegisterPlayerSession(IPlayerSession session)
|
||||
{
|
||||
if (_player == session)
|
||||
{
|
||||
DeactivePlayer(_player);
|
||||
}
|
||||
else
|
||||
{
|
||||
_spectators.Remove(session);
|
||||
UpdatePlayerStatus(session);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePlayerStatus(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.SendMessage(new BlockGameMessages.BlockGameUserStatusMessage(_player == session), session);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
UserInterface.OnClosed += UnRegisterPlayerSession;
|
||||
}
|
||||
_game = new BlockGame(this);
|
||||
}
|
||||
|
||||
private void OnPowerStateChanged(PowerChangedMessage e)
|
||||
{
|
||||
if (e.Powered) return;
|
||||
|
||||
UserInterface?.CloseAll();
|
||||
_player = null;
|
||||
_spectators.Clear();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
switch (obj.Message)
|
||||
{
|
||||
case BlockGameMessages.BlockGamePlayerActionMessage playerActionMessage:
|
||||
if (obj.Session != _player) break;
|
||||
|
||||
if (!ActionBlockerSystem.CanInteract(Owner))
|
||||
{
|
||||
DeactivePlayer(obj.Session);
|
||||
break;
|
||||
}
|
||||
|
||||
if (playerActionMessage.PlayerAction == BlockGamePlayerAction.NewGame)
|
||||
{
|
||||
if(_game?.Started == true) _game = new BlockGame(this);
|
||||
_game?.StartGame();
|
||||
}
|
||||
else
|
||||
{
|
||||
_game?.ProcessInput(playerActionMessage.PlayerAction);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void DoGameTick(float frameTime)
|
||||
{
|
||||
_game?.GameTick(frameTime);
|
||||
}
|
||||
|
||||
private class BlockGame
|
||||
{
|
||||
//note: field is 10(0 -> 9) wide and 20(0 -> 19) high
|
||||
|
||||
private readonly BlockGameArcadeComponent _component;
|
||||
|
||||
private readonly List<BlockGameBlock> _field = new();
|
||||
|
||||
private BlockGamePiece _currentPiece;
|
||||
|
||||
private BlockGamePiece _nextPiece
|
||||
{
|
||||
get => _internalNextPiece;
|
||||
set
|
||||
{
|
||||
_internalNextPiece = value;
|
||||
SendNextPieceUpdate();
|
||||
}
|
||||
}
|
||||
private BlockGamePiece _internalNextPiece;
|
||||
|
||||
private void SendNextPieceUpdate()
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
|
||||
}
|
||||
|
||||
private void SendNextPieceUpdate(IPlayerSession session)
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock), session);
|
||||
}
|
||||
|
||||
private bool _holdBlock = false;
|
||||
private BlockGamePiece? _heldPiece
|
||||
{
|
||||
get => _internalHeldPiece;
|
||||
set
|
||||
{
|
||||
_internalHeldPiece = value;
|
||||
SendHoldPieceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private BlockGamePiece? _internalHeldPiece = null;
|
||||
|
||||
private void SendHoldPieceUpdate()
|
||||
{
|
||||
if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock));
|
||||
else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock));
|
||||
}
|
||||
|
||||
private void SendHoldPieceUpdate(IPlayerSession session)
|
||||
{
|
||||
if(_heldPiece.HasValue) _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_heldPiece.Value.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.HoldBlock), session);
|
||||
else _component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(new BlockGameBlock[0], BlockGameMessages.BlockGameVisualType.HoldBlock), session);
|
||||
}
|
||||
|
||||
private Vector2i _currentPiecePosition;
|
||||
private BlockGamePieceRotation _currentRotation;
|
||||
private float _softDropModifier = 0.1f;
|
||||
|
||||
private float Speed =>
|
||||
-0.03f * Level + 1 * (!_softDropPressed ? 1 : _softDropModifier);
|
||||
|
||||
private const float _pressCheckSpeed = 0.08f;
|
||||
|
||||
private bool _running;
|
||||
public bool Paused => !(_running && _started);
|
||||
private bool _started;
|
||||
public bool Started => _started;
|
||||
private bool _gameOver;
|
||||
|
||||
private bool _leftPressed;
|
||||
private bool _rightPressed;
|
||||
private bool _softDropPressed;
|
||||
|
||||
private int Points
|
||||
{
|
||||
get => _internalPoints;
|
||||
set
|
||||
{
|
||||
if (_internalPoints == value) return;
|
||||
_internalPoints = value;
|
||||
SendPointsUpdate();
|
||||
}
|
||||
}
|
||||
private int _internalPoints;
|
||||
|
||||
private BlockGameSystem.HighScorePlacement? _highScorePlacement = null;
|
||||
|
||||
private void SendPointsUpdate()
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points));
|
||||
}
|
||||
|
||||
private void SendPointsUpdate(IPlayerSession session)
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameScoreUpdateMessage(Points));
|
||||
}
|
||||
|
||||
public int Level
|
||||
{
|
||||
get => _level;
|
||||
set
|
||||
{
|
||||
_level = value;
|
||||
SendLevelUpdate();
|
||||
}
|
||||
}
|
||||
private int _level = 0;
|
||||
private void SendLevelUpdate()
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level));
|
||||
}
|
||||
|
||||
private void SendLevelUpdate(IPlayerSession session)
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameLevelUpdateMessage(Level));
|
||||
}
|
||||
|
||||
private int ClearedLines
|
||||
{
|
||||
get => _clearedLines;
|
||||
set
|
||||
{
|
||||
_clearedLines = value;
|
||||
|
||||
if (_clearedLines < LevelRequirement) return;
|
||||
|
||||
_clearedLines -= LevelRequirement;
|
||||
Level++;
|
||||
}
|
||||
}
|
||||
|
||||
private int _clearedLines = 0;
|
||||
private int LevelRequirement => Math.Min(100, Math.Max(Level * 10 - 50, 10));
|
||||
|
||||
public BlockGame(BlockGameArcadeComponent component)
|
||||
{
|
||||
_component = component;
|
||||
_allBlockGamePieces = (BlockGamePieceType[]) Enum.GetValues(typeof(BlockGamePieceType));
|
||||
_internalNextPiece = GetRandomBlockGamePiece(_component._random);
|
||||
InitializeNewBlock();
|
||||
}
|
||||
|
||||
private void SendHighscoreUpdate()
|
||||
{
|
||||
var entitySystem = EntitySystem.Get<BlockGameSystem>();
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()));
|
||||
}
|
||||
|
||||
private void SendHighscoreUpdate(IPlayerSession session)
|
||||
{
|
||||
var entitySystem = EntitySystem.Get<BlockGameSystem>();
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameHighScoreUpdateMessage(entitySystem.GetLocalHighscores(), entitySystem.GetGlobalHighscores()), session);
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
|
||||
|
||||
FullUpdate();
|
||||
|
||||
_running = true;
|
||||
_started = true;
|
||||
}
|
||||
|
||||
private void FullUpdate()
|
||||
{
|
||||
UpdateAllFieldUI();
|
||||
SendHoldPieceUpdate();
|
||||
SendNextPieceUpdate();
|
||||
SendPointsUpdate();
|
||||
SendHighscoreUpdate();
|
||||
SendLevelUpdate();
|
||||
}
|
||||
|
||||
private void FullUpdate(IPlayerSession session)
|
||||
{
|
||||
UpdateFieldUI(session);
|
||||
SendPointsUpdate(session);
|
||||
SendNextPieceUpdate(session);
|
||||
SendHoldPieceUpdate(session);
|
||||
SendHighscoreUpdate(session);
|
||||
SendLevelUpdate(session);
|
||||
}
|
||||
|
||||
public void GameTick(float frameTime)
|
||||
{
|
||||
if (!_running) return;
|
||||
|
||||
InputTick(frameTime);
|
||||
|
||||
FieldTick(frameTime);
|
||||
}
|
||||
|
||||
private float _accumulatedLeftPressTime;
|
||||
private float _accumulatedRightPressTime;
|
||||
private void InputTick(float frameTime)
|
||||
{
|
||||
bool anythingChanged = false;
|
||||
if (_leftPressed)
|
||||
{
|
||||
_accumulatedLeftPressTime += frameTime;
|
||||
|
||||
while (_accumulatedLeftPressTime >= _pressCheckSpeed)
|
||||
{
|
||||
|
||||
if (_currentPiece.Positions(_currentPiecePosition.AddToX(-1), _currentRotation)
|
||||
.All(MoveCheck))
|
||||
{
|
||||
_currentPiecePosition = _currentPiecePosition.AddToX(-1);
|
||||
anythingChanged = true;
|
||||
}
|
||||
|
||||
_accumulatedLeftPressTime -= _pressCheckSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
if (_rightPressed)
|
||||
{
|
||||
_accumulatedRightPressTime += frameTime;
|
||||
|
||||
while (_accumulatedRightPressTime >= _pressCheckSpeed)
|
||||
{
|
||||
if (_currentPiece.Positions(_currentPiecePosition.AddToX(1), _currentRotation)
|
||||
.All(MoveCheck))
|
||||
{
|
||||
_currentPiecePosition = _currentPiecePosition.AddToX(1);
|
||||
anythingChanged = true;
|
||||
}
|
||||
|
||||
_accumulatedRightPressTime -= _pressCheckSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
if(anythingChanged) UpdateAllFieldUI();
|
||||
}
|
||||
|
||||
private float _accumulatedFieldFrameTime;
|
||||
private void FieldTick(float frameTime)
|
||||
{
|
||||
_accumulatedFieldFrameTime += frameTime;
|
||||
|
||||
var checkTime = Speed;
|
||||
|
||||
while (_accumulatedFieldFrameTime >= checkTime)
|
||||
{
|
||||
if (_softDropPressed) AddPoints(1);
|
||||
|
||||
InternalFieldTick();
|
||||
|
||||
_accumulatedFieldFrameTime -= checkTime;
|
||||
}
|
||||
}
|
||||
|
||||
private void InternalFieldTick()
|
||||
{
|
||||
if (_currentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation)
|
||||
.All(DropCheck))
|
||||
{
|
||||
_currentPiecePosition = _currentPiecePosition.AddToY(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
var blocks = _currentPiece.Blocks(_currentPiecePosition, _currentRotation);
|
||||
_field.AddRange(blocks);
|
||||
|
||||
//check loose conditions
|
||||
if (IsGameOver)
|
||||
{
|
||||
InvokeGameover();
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeNewBlock();
|
||||
}
|
||||
|
||||
CheckField();
|
||||
|
||||
UpdateAllFieldUI();
|
||||
}
|
||||
|
||||
private void CheckField()
|
||||
{
|
||||
int pointsToAdd = 0;
|
||||
int consecutiveLines = 0;
|
||||
int clearedLines = 0;
|
||||
for (int y = 0; y < 20; y++)
|
||||
{
|
||||
if (CheckLine(y))
|
||||
{
|
||||
//line was cleared
|
||||
y--;
|
||||
consecutiveLines++;
|
||||
clearedLines++;
|
||||
}
|
||||
else if(consecutiveLines != 0)
|
||||
{
|
||||
var mod = consecutiveLines switch
|
||||
{
|
||||
1 => 40,
|
||||
2 => 100,
|
||||
3 => 300,
|
||||
4 => 1200,
|
||||
_ => 0
|
||||
};
|
||||
pointsToAdd += mod * (_level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
ClearedLines += clearedLines;
|
||||
AddPoints(pointsToAdd);
|
||||
}
|
||||
|
||||
private bool CheckLine(int y)
|
||||
{
|
||||
for (var x = 0; x < 10; x++)
|
||||
{
|
||||
if (!_field.Any(b => b.Position.X == x && b.Position.Y == y)) return false;
|
||||
}
|
||||
|
||||
//clear line
|
||||
_field.RemoveAll(b => b.Position.Y == y);
|
||||
//move everything down
|
||||
FillLine(y);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddPoints(int amount)
|
||||
{
|
||||
if (amount == 0) return;
|
||||
|
||||
Points += amount;
|
||||
}
|
||||
|
||||
private void FillLine(int y)
|
||||
{
|
||||
for (int c_y = y; c_y > 0; c_y--)
|
||||
{
|
||||
for (int j = 0; j < _field.Count; j++)
|
||||
{
|
||||
if(_field[j].Position.Y != c_y-1) continue;
|
||||
|
||||
_field[j] = new BlockGameBlock(_field[j].Position.AddToY(1), _field[j].GameBlockColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeNewBlock()
|
||||
{
|
||||
InitializeNewBlock(_nextPiece);
|
||||
_nextPiece = GetRandomBlockGamePiece(_component._random);
|
||||
_holdBlock = false;
|
||||
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(_nextPiece.BlocksForPreview(), BlockGameMessages.BlockGameVisualType.NextBlock));
|
||||
}
|
||||
|
||||
private void InitializeNewBlock(BlockGamePiece piece)
|
||||
{
|
||||
_currentPiecePosition = new Vector2i(5,0);
|
||||
|
||||
_currentRotation = BlockGamePieceRotation.North;
|
||||
|
||||
_currentPiece = piece;
|
||||
UpdateAllFieldUI();
|
||||
}
|
||||
|
||||
private bool LowerBoundCheck(Vector2i position) => position.Y < 20;
|
||||
private bool BorderCheck(Vector2i position) => position.X >= 0 && position.X < 10;
|
||||
private bool ClearCheck(Vector2i position) => _field.All(block => !position.Equals(block.Position));
|
||||
|
||||
private bool DropCheck(Vector2i position) => LowerBoundCheck(position) && ClearCheck(position);
|
||||
private bool MoveCheck(Vector2i position) => BorderCheck(position) && ClearCheck(position);
|
||||
private bool RotateCheck(Vector2i position) => BorderCheck(position) && LowerBoundCheck(position) && ClearCheck(position);
|
||||
|
||||
public void ProcessInput(BlockGamePlayerAction action)
|
||||
{
|
||||
if (_running)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case BlockGamePlayerAction.StartLeft:
|
||||
_leftPressed = true;
|
||||
break;
|
||||
case BlockGamePlayerAction.StartRight:
|
||||
_rightPressed = true;
|
||||
break;
|
||||
case BlockGamePlayerAction.Rotate:
|
||||
TrySetRotation(Next(_currentRotation, false));
|
||||
break;
|
||||
case BlockGamePlayerAction.CounterRotate:
|
||||
TrySetRotation(Next(_currentRotation, true));
|
||||
break;
|
||||
case BlockGamePlayerAction.SoftdropStart:
|
||||
_softDropPressed = true;
|
||||
if (_accumulatedFieldFrameTime > Speed) _accumulatedFieldFrameTime = Speed; //to prevent jumps
|
||||
break;
|
||||
case BlockGamePlayerAction.Harddrop:
|
||||
PerformHarddrop();
|
||||
break;
|
||||
case BlockGamePlayerAction.Hold:
|
||||
HoldPiece();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case BlockGamePlayerAction.EndLeft:
|
||||
_leftPressed = false;
|
||||
break;
|
||||
case BlockGamePlayerAction.EndRight:
|
||||
_rightPressed = false;
|
||||
break;
|
||||
case BlockGamePlayerAction.SoftdropEnd:
|
||||
_softDropPressed = false;
|
||||
break;
|
||||
case BlockGamePlayerAction.Pause:
|
||||
_running = false;
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, _started));
|
||||
break;
|
||||
case BlockGamePlayerAction.Unpause:
|
||||
if (!_gameOver && _started)
|
||||
{
|
||||
_running = true;
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game));
|
||||
}
|
||||
break;
|
||||
case BlockGamePlayerAction.ShowHighscores:
|
||||
_running = false;
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Highscores, _started));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void TrySetRotation(BlockGamePieceRotation rotation)
|
||||
{
|
||||
if(!_running) return;
|
||||
|
||||
if (!_currentPiece.CanSpin) return;
|
||||
|
||||
if (!_currentPiece.Positions(_currentPiecePosition, rotation)
|
||||
.All(RotateCheck))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_currentRotation = rotation;
|
||||
UpdateAllFieldUI();
|
||||
}
|
||||
|
||||
private void HoldPiece()
|
||||
{
|
||||
if (!_running) return;
|
||||
|
||||
if (_holdBlock) return;
|
||||
|
||||
var tempHeld = _heldPiece;
|
||||
_heldPiece = _currentPiece;
|
||||
_holdBlock = true;
|
||||
|
||||
if (!tempHeld.HasValue)
|
||||
{
|
||||
InitializeNewBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeNewBlock(tempHeld.Value);
|
||||
}
|
||||
|
||||
private void PerformHarddrop()
|
||||
{
|
||||
int spacesDropped = 0;
|
||||
while (_currentPiece.Positions(_currentPiecePosition.AddToY(1), _currentRotation)
|
||||
.All(DropCheck))
|
||||
{
|
||||
_currentPiecePosition = _currentPiecePosition.AddToY(1);
|
||||
spacesDropped++;
|
||||
}
|
||||
AddPoints(spacesDropped * 2);
|
||||
|
||||
InternalFieldTick();
|
||||
}
|
||||
|
||||
public void UpdateAllFieldUI()
|
||||
{
|
||||
if (!_started) return;
|
||||
|
||||
var computedField = ComputeField();
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField));
|
||||
}
|
||||
|
||||
public void UpdateFieldUI(IPlayerSession session)
|
||||
{
|
||||
if (!_started) return;
|
||||
|
||||
var computedField = ComputeField();
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameVisualUpdateMessage(computedField.ToArray(), BlockGameMessages.BlockGameVisualType.GameField), session);
|
||||
}
|
||||
|
||||
private bool IsGameOver => _field.Any(block => block.Position.Y == 0);
|
||||
|
||||
private void InvokeGameover()
|
||||
{
|
||||
_running = false;
|
||||
_gameOver = true;
|
||||
|
||||
if (_component._player?.AttachedEntity != null)
|
||||
{
|
||||
var blockGameSystem = EntitySystem.Get<BlockGameSystem>();
|
||||
|
||||
_highScorePlacement = blockGameSystem.RegisterHighScore(_component._player.AttachedEntity.Name, Points);
|
||||
SendHighscoreUpdate();
|
||||
}
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement));
|
||||
}
|
||||
|
||||
public void UpdateNewPlayerUI(IPlayerSession session)
|
||||
{
|
||||
if (_gameOver)
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameGameOverScreenMessage(Points, _highScorePlacement?.LocalPlacement, _highScorePlacement?.GlobalPlacement), session);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Paused)
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Pause, Started), session);
|
||||
}
|
||||
else
|
||||
{
|
||||
_component.UserInterface?.SendMessage(new BlockGameMessages.BlockGameSetScreenMessage(BlockGameMessages.BlockGameScreen.Game, Started), session);
|
||||
}
|
||||
}
|
||||
|
||||
FullUpdate(session);
|
||||
}
|
||||
|
||||
public List<BlockGameBlock> ComputeField()
|
||||
{
|
||||
var result = new List<BlockGameBlock>();
|
||||
result.AddRange(_field);
|
||||
result.AddRange(_currentPiece.Blocks(_currentPiecePosition, _currentRotation));
|
||||
return result;
|
||||
}
|
||||
|
||||
private enum BlockGamePieceType
|
||||
{
|
||||
I,
|
||||
L,
|
||||
LInverted,
|
||||
S,
|
||||
SInverted,
|
||||
T,
|
||||
O
|
||||
}
|
||||
|
||||
private enum BlockGamePieceRotation
|
||||
{
|
||||
North,
|
||||
East,
|
||||
South,
|
||||
West
|
||||
}
|
||||
|
||||
private static BlockGamePieceRotation Next(BlockGamePieceRotation rotation, bool inverted)
|
||||
{
|
||||
return rotation switch
|
||||
{
|
||||
BlockGamePieceRotation.North => inverted ? BlockGamePieceRotation.West : BlockGamePieceRotation.East,
|
||||
BlockGamePieceRotation.East => inverted ? BlockGamePieceRotation.North : BlockGamePieceRotation.South,
|
||||
BlockGamePieceRotation.South => inverted ? BlockGamePieceRotation.East : BlockGamePieceRotation.West,
|
||||
BlockGamePieceRotation.West => inverted ? BlockGamePieceRotation.South : BlockGamePieceRotation.North,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(rotation), rotation, null)
|
||||
};
|
||||
}
|
||||
|
||||
private readonly BlockGamePieceType[] _allBlockGamePieces;
|
||||
|
||||
private List<BlockGamePieceType> _blockGamePiecesBuffer = new();
|
||||
|
||||
private BlockGamePiece GetRandomBlockGamePiece(IRobustRandom random)
|
||||
{
|
||||
if (_blockGamePiecesBuffer.Count == 0)
|
||||
{
|
||||
_blockGamePiecesBuffer = _allBlockGamePieces.ToList();
|
||||
}
|
||||
|
||||
var chosenPiece = random.Pick(_blockGamePiecesBuffer);
|
||||
_blockGamePiecesBuffer.Remove(chosenPiece);
|
||||
return BlockGamePiece.GetPiece(chosenPiece);
|
||||
}
|
||||
|
||||
private struct BlockGamePiece
|
||||
{
|
||||
public Vector2i[] Offsets;
|
||||
private BlockGameBlock.BlockGameBlockColor _gameBlockColor;
|
||||
public bool CanSpin;
|
||||
|
||||
public Vector2i[] Positions(Vector2i center,
|
||||
BlockGamePieceRotation rotation)
|
||||
{
|
||||
return RotatedOffsets(rotation).Select(v => center + v).ToArray();
|
||||
}
|
||||
|
||||
private Vector2i[] RotatedOffsets(BlockGamePieceRotation rotation)
|
||||
{
|
||||
Vector2i[] rotatedOffsets = (Vector2i[])Offsets.Clone();
|
||||
//until i find a better algo
|
||||
var amount = rotation switch
|
||||
{
|
||||
BlockGamePieceRotation.North => 0,
|
||||
BlockGamePieceRotation.East => 1,
|
||||
BlockGamePieceRotation.South => 2,
|
||||
BlockGamePieceRotation.West => 3,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
for (var j = 0; j < rotatedOffsets.Length; j++)
|
||||
{
|
||||
rotatedOffsets[j] = rotatedOffsets[j].Rotate90DegreesAsOffset();
|
||||
}
|
||||
}
|
||||
|
||||
return rotatedOffsets;
|
||||
}
|
||||
|
||||
public BlockGameBlock[] Blocks(Vector2i center,
|
||||
BlockGamePieceRotation rotation)
|
||||
{
|
||||
var positions = Positions(center, rotation);
|
||||
var result = new BlockGameBlock[positions.Length];
|
||||
var i = 0;
|
||||
foreach (var position in positions)
|
||||
{
|
||||
result[i++] = position.ToBlockGameBlock(_gameBlockColor);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public BlockGameBlock[] BlocksForPreview()
|
||||
{
|
||||
var xOffset = 0;
|
||||
var yOffset = 0;
|
||||
foreach (var offset in Offsets)
|
||||
{
|
||||
if (offset.X < xOffset) xOffset = offset.X;
|
||||
if (offset.Y < yOffset) yOffset = offset.Y;
|
||||
}
|
||||
|
||||
return Blocks(new Vector2i(-xOffset, -yOffset), BlockGamePieceRotation.North);
|
||||
}
|
||||
|
||||
public static BlockGamePiece GetPiece(BlockGamePieceType type)
|
||||
{
|
||||
//switch statement, hardcoded offsets
|
||||
return type switch
|
||||
{
|
||||
BlockGamePieceType.I => new BlockGamePiece
|
||||
{
|
||||
Offsets = new[]
|
||||
{
|
||||
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(0, 2),
|
||||
},
|
||||
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.LightBlue,
|
||||
CanSpin = true
|
||||
},
|
||||
BlockGamePieceType.L => new BlockGamePiece
|
||||
{
|
||||
Offsets = new[]
|
||||
{
|
||||
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(0, 1), new Vector2i(1, 1),
|
||||
},
|
||||
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Orange,
|
||||
CanSpin = true
|
||||
},
|
||||
BlockGamePieceType.LInverted => new BlockGamePiece
|
||||
{
|
||||
Offsets = new[]
|
||||
{
|
||||
new Vector2i(0, -1), new Vector2i(0, 0), new Vector2i(-1, 1),
|
||||
new Vector2i(0, 1),
|
||||
},
|
||||
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Blue,
|
||||
CanSpin = true
|
||||
},
|
||||
BlockGamePieceType.S => new BlockGamePiece
|
||||
{
|
||||
Offsets = new[]
|
||||
{
|
||||
new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(-1, 0),
|
||||
new Vector2i(0, 0),
|
||||
},
|
||||
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Green,
|
||||
CanSpin = true
|
||||
},
|
||||
BlockGamePieceType.SInverted => new BlockGamePiece
|
||||
{
|
||||
Offsets = new[]
|
||||
{
|
||||
new Vector2i(-1, -1), new Vector2i(0, -1), new Vector2i(0, 0),
|
||||
new Vector2i(1, 0),
|
||||
},
|
||||
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Red,
|
||||
CanSpin = true
|
||||
},
|
||||
BlockGamePieceType.T => new BlockGamePiece
|
||||
{
|
||||
Offsets = new[]
|
||||
{
|
||||
new Vector2i(0, -1),
|
||||
new Vector2i(-1, 0), new Vector2i(0, 0), new Vector2i(1, 0),
|
||||
},
|
||||
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Purple,
|
||||
CanSpin = true
|
||||
},
|
||||
BlockGamePieceType.O => new BlockGamePiece
|
||||
{
|
||||
Offsets = new[]
|
||||
{
|
||||
new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(0, 0),
|
||||
new Vector2i(1, 0),
|
||||
},
|
||||
_gameBlockColor = BlockGameBlock.BlockGameBlockColor.Yellow,
|
||||
CanSpin = false
|
||||
},
|
||||
_ => new BlockGamePiece {Offsets = new[] {new Vector2i(0, 0)}}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Arcade
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class RandomArcadeGameComponent : Component, IMapInit
|
||||
{
|
||||
public override string Name => "RandomArcade";
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
var arcades = new[]
|
||||
{
|
||||
"BlockGameArcade",
|
||||
"SpaceVillainArcade"
|
||||
};
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
entityManager.SpawnEntity(
|
||||
IoCManager.Resolve<IRobustRandom>().Pick(arcades),
|
||||
Owner.Transform.Coordinates);
|
||||
|
||||
Owner.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Server.GameObjects.Components.VendingMachines;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Components.Arcade;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Arcade
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
public class SpaceVillainArcadeComponent : SharedSpaceVillainArcadeComponent, IActivate, IWires
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = null!;
|
||||
|
||||
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
|
||||
[ComponentDependency] private readonly WiresComponent? _wiresComponent = default!;
|
||||
|
||||
private bool Powered => _powerReceiverComponent != null && _powerReceiverComponent.Powered;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SpaceVillainArcadeUiKey.Key);
|
||||
[ViewVariables] private bool _overflowFlag;
|
||||
[ViewVariables] private bool _playerInvincibilityFlag;
|
||||
[ViewVariables] private bool _enemyInvincibilityFlag;
|
||||
[ViewVariables] private SpaceVillainGame _game = null!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleFightVerbs")] private List<string> _possibleFightVerbs = new List<string>()
|
||||
{"Defeat", "Annihilate", "Save", "Strike", "Stop", "Destroy", "Robust", "Romance", "Pwn", "Own"};
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleFirstEnemyNames")] private List<string> _possibleFirstEnemyNames = new List<string>(){
|
||||
"the Automatic", "Farmer", "Lord", "Professor", "the Cuban", "the Evil", "the Dread King",
|
||||
"the Space", "Lord", "the Great", "Duke", "General"
|
||||
};
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleLastEnemyNames")] private List<string> _possibleLastEnemyNames = new List<string>()
|
||||
{
|
||||
"Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid",
|
||||
"Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn"
|
||||
};
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("possibleRewards")] private List<string> _possibleRewards = new List<string>()
|
||||
{
|
||||
"ToyMouse", "ToyAi", "ToyNuke", "ToyAssistant", "ToyGriffin", "ToyHonk", "ToyIan",
|
||||
"ToyMarauder", "ToyMauler", "ToyGygax", "ToyOdysseus", "ToyOwlman", "ToyDeathRipley",
|
||||
"ToyPhazon", "ToyFireRipley", "ToyReticence", "ToyRipley", "ToySeraph", "ToyDurand", "ToySkeleton"
|
||||
};
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if(!eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!Powered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(!ActionBlockerSystem.CanInteract(actor.PlayerSession.AttachedEntity)) return;
|
||||
|
||||
_game ??= new SpaceVillainGame(this);
|
||||
|
||||
if (_wiresComponent?.IsPanelOpen == true)
|
||||
{
|
||||
_wiresComponent.OpenInterface(actor.PlayerSession);
|
||||
} else
|
||||
{
|
||||
UserInterface?.Toggle(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
OnOnPowerStateChanged(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOnPowerStateChanged(PowerChangedMessage e)
|
||||
{
|
||||
if(e.Powered) return;
|
||||
|
||||
UserInterface?.CloseAll();
|
||||
}
|
||||
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
|
||||
{
|
||||
if (!Powered)
|
||||
return;
|
||||
|
||||
if (serverMsg.Message is not SpaceVillainArcadePlayerActionMessage msg) return;
|
||||
|
||||
switch (msg.PlayerAction)
|
||||
{
|
||||
case PlayerAction.Attack:
|
||||
_game?.ExecutePlayerAction(msg.PlayerAction);
|
||||
break;
|
||||
case PlayerAction.Heal:
|
||||
_game?.ExecutePlayerAction(msg.PlayerAction);
|
||||
break;
|
||||
case PlayerAction.Recharge:
|
||||
_game?.ExecutePlayerAction(msg.PlayerAction);
|
||||
break;
|
||||
case PlayerAction.NewGame:
|
||||
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Effects/Arcade/newgame.ogg", Owner, AudioParams.Default.WithVolume(-4f));
|
||||
_game = new SpaceVillainGame(this);
|
||||
UserInterface?.SendMessage(_game.GenerateMetaDataMessage());
|
||||
break;
|
||||
case PlayerAction.RequestData:
|
||||
UserInterface?.SendMessage(_game.GenerateMetaDataMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Wires
|
||||
{
|
||||
/// <summary>
|
||||
/// Disables Max Health&Mana for both Enemy and Player.
|
||||
/// </summary>
|
||||
Overflow,
|
||||
/// <summary>
|
||||
/// Makes Player Invincible.
|
||||
/// </summary>
|
||||
PlayerInvincible,
|
||||
/// <summary>
|
||||
/// Makes Enemy Invincible.
|
||||
/// </summary>
|
||||
EnemyInvincible
|
||||
}
|
||||
|
||||
public void RegisterWires(WiresComponent.WiresBuilder builder)
|
||||
{
|
||||
builder.CreateWire(Wires.Overflow);
|
||||
builder.CreateWire(Wires.PlayerInvincible);
|
||||
builder.CreateWire(Wires.EnemyInvincible);
|
||||
builder.CreateWire(4);
|
||||
builder.CreateWire(5);
|
||||
builder.CreateWire(6);
|
||||
IndicatorUpdate();
|
||||
}
|
||||
|
||||
public void WiresUpdate(WiresUpdateEventArgs args)
|
||||
{
|
||||
var wire = (Wires) args.Identifier;
|
||||
var value = args.Action != SharedWiresComponent.WiresAction.Mend;
|
||||
switch (wire)
|
||||
{
|
||||
case Wires.Overflow:
|
||||
_overflowFlag = value;
|
||||
break;
|
||||
case Wires.PlayerInvincible:
|
||||
_playerInvincibilityFlag = value;
|
||||
break;
|
||||
case Wires.EnemyInvincible:
|
||||
_enemyInvincibilityFlag = value;
|
||||
break;
|
||||
}
|
||||
|
||||
IndicatorUpdate();
|
||||
}
|
||||
|
||||
public void IndicatorUpdate()
|
||||
{
|
||||
_wiresComponent?.SetStatus(Indicators.HealthManager,
|
||||
new SharedWiresComponent.StatusLightData(Color.Purple,
|
||||
_playerInvincibilityFlag || _enemyInvincibilityFlag
|
||||
? SharedWiresComponent.StatusLightState.BlinkingSlow
|
||||
: SharedWiresComponent.StatusLightState.On,
|
||||
"MNGR"));
|
||||
_wiresComponent?.SetStatus(Indicators.HealthLimiter,
|
||||
new SharedWiresComponent.StatusLightData(Color.Red,
|
||||
_overflowFlag
|
||||
? SharedWiresComponent.StatusLightState.BlinkingSlow
|
||||
: SharedWiresComponent.StatusLightState.On,
|
||||
"LIMT"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the user wins the game.
|
||||
/// </summary>
|
||||
public void ProcessWin()
|
||||
{
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
entityManager.SpawnEntity(_random.Pick(_possibleRewards), Owner.Transform.MapPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks a fight-verb from the list of possible Verbs.
|
||||
/// </summary>
|
||||
/// <returns>A fight-verb.</returns>
|
||||
public string GenerateFightVerb()
|
||||
{
|
||||
return _random.Pick(_possibleFightVerbs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an enemy-name comprised of a first- and last-name.
|
||||
/// </summary>
|
||||
/// <returns>An enemy-name.</returns>
|
||||
public string GenerateEnemyName()
|
||||
{
|
||||
return $"{_random.Pick(_possibleFirstEnemyNames)} {_random.Pick(_possibleLastEnemyNames)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Class to handle all the game-logic of the SpaceVillain-game.
|
||||
/// </summary>
|
||||
public class SpaceVillainGame
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
[ViewVariables] private readonly SpaceVillainArcadeComponent _owner;
|
||||
|
||||
[ViewVariables] public string Name => $"{_fightVerb} {_enemyName}";
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _playerHp = 30;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _playerHpMax = 30;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _playerMp = 10;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _playerMpMax = 10;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _enemyHp = 45;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _enemyHpMax = 45;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _enemyMp = 20;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _enemyMpMax = 20;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _turtleTracker;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)] private readonly string _fightVerb;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private readonly string _enemyName;
|
||||
|
||||
[ViewVariables] private bool _running = true;
|
||||
|
||||
private string _latestPlayerActionMessage = "";
|
||||
private string _latestEnemyActionMessage = "";
|
||||
|
||||
public SpaceVillainGame(SpaceVillainArcadeComponent owner) : this(owner, owner.GenerateFightVerb(), owner.GenerateEnemyName()){}
|
||||
|
||||
public SpaceVillainGame(SpaceVillainArcadeComponent owner, string fightVerb, string enemyName)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_owner = owner;
|
||||
//todo defeat the curse secret game mode
|
||||
_fightVerb = fightVerb;
|
||||
_enemyName = enemyName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates all vars incase they overshoot their max-values.
|
||||
/// Does not check if vars surpass 0.
|
||||
/// </summary>
|
||||
private void ValidateVars()
|
||||
{
|
||||
if(_owner._overflowFlag) return;
|
||||
|
||||
if (_playerHp > _playerHpMax) _playerHp = _playerHpMax;
|
||||
if (_playerMp > _playerMpMax) _playerMp = _playerMpMax;
|
||||
if (_enemyHp > _enemyHpMax) _enemyHp = _enemyHpMax;
|
||||
if (_enemyMp > _enemyMpMax) _enemyMp = _enemyMpMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the SpaceVillainArcadeComponent when Userinput is received.
|
||||
/// </summary>
|
||||
/// <param name="action">The action the user picked.</param>
|
||||
public void ExecutePlayerAction(PlayerAction action)
|
||||
{
|
||||
if (!_running) return;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case PlayerAction.Attack:
|
||||
var attackAmount = _random.Next(2, 6);
|
||||
_latestPlayerActionMessage = Loc.GetString("You attack {0} for {1}!", _enemyName, attackAmount);
|
||||
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/player_attack.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
|
||||
if(!_owner._enemyInvincibilityFlag) _enemyHp -= attackAmount;
|
||||
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
|
||||
break;
|
||||
case PlayerAction.Heal:
|
||||
var pointAmount = _random.Next(1, 3);
|
||||
var healAmount = _random.Next(6, 8);
|
||||
_latestPlayerActionMessage = Loc.GetString("You use {0} magic to heal for {1} damage!", pointAmount, healAmount);
|
||||
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/player_heal.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
|
||||
if(!_owner._playerInvincibilityFlag) _playerMp -= pointAmount;
|
||||
_playerHp += healAmount;
|
||||
_turtleTracker++;
|
||||
break;
|
||||
case PlayerAction.Recharge:
|
||||
var chargeAmount = _random.Next(4, 7);
|
||||
_latestPlayerActionMessage = Loc.GetString("You regain {0} points", chargeAmount);
|
||||
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/player_charge.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
|
||||
_playerMp += chargeAmount;
|
||||
_turtleTracker -= _turtleTracker > 0 ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!CheckGameConditions())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateVars();
|
||||
ExecuteAiAction();
|
||||
|
||||
if (!CheckGameConditions())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ValidateVars();
|
||||
UpdateUi();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the Game conditions and Updates the Ui & Plays a sound accordingly.
|
||||
/// </summary>
|
||||
/// <returns>A bool indicating if the game should continue.</returns>
|
||||
private bool CheckGameConditions()
|
||||
{
|
||||
if ((_playerHp > 0 && _playerMp > 0) && (_enemyHp <= 0 || _enemyMp <= 0))
|
||||
{
|
||||
_running = false;
|
||||
UpdateUi(Loc.GetString("You won!"), Loc.GetString("{0} dies.", _enemyName), true);
|
||||
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/win.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
|
||||
_owner.ProcessWin();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_playerHp > 0 && _playerMp > 0) return true;
|
||||
|
||||
if ((_enemyHp > 0 && _enemyMp > 0))
|
||||
{
|
||||
_running = false;
|
||||
UpdateUi(Loc.GetString("You lost!"), Loc.GetString("{0} cheers.", _enemyName), true);
|
||||
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/gameover.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
|
||||
return false;
|
||||
}
|
||||
if (_enemyHp <= 0 || _enemyMp <= 0)
|
||||
{
|
||||
_running = false;
|
||||
UpdateUi(Loc.GetString("You lost!"), Loc.GetString("{0} dies, but takes you with him.", _enemyName), true);
|
||||
SoundSystem.Play(Filter.Pvs(_owner.Owner), "/Audio/Effects/Arcade/gameover.ogg", _owner.Owner, AudioParams.Default.WithVolume(-4f));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the UI.
|
||||
/// </summary>
|
||||
private void UpdateUi(bool metadata = false)
|
||||
{
|
||||
_owner.UserInterface?.SendMessage(metadata ? GenerateMetaDataMessage() : GenerateUpdateMessage());
|
||||
}
|
||||
|
||||
private void UpdateUi(string message1, string message2, bool metadata = false)
|
||||
{
|
||||
_latestPlayerActionMessage = message1;
|
||||
_latestEnemyActionMessage = message2;
|
||||
UpdateUi(metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the logic of the AI
|
||||
/// </summary>
|
||||
/// <returns>An Enemyaction-message.</returns>
|
||||
private void ExecuteAiAction()
|
||||
{
|
||||
if (_turtleTracker >= 4)
|
||||
{
|
||||
var boomAmount = _random.Next(5, 10);
|
||||
_latestEnemyActionMessage = Loc.GetString("{0} throws a bomb, exploding you for {1} damage!", _enemyName, boomAmount);
|
||||
if (_owner._playerInvincibilityFlag) return;
|
||||
_playerHp -= boomAmount;
|
||||
_turtleTracker--;
|
||||
}else if (_enemyMp <= 5 && _random.Prob(0.7f))
|
||||
{
|
||||
var stealAmount = _random.Next(2, 3);
|
||||
_latestEnemyActionMessage = Loc.GetString("{0} steals {1} of your power!", _enemyName, stealAmount);
|
||||
if (_owner._playerInvincibilityFlag) return;
|
||||
_playerMp -= stealAmount;
|
||||
_enemyMp += stealAmount;
|
||||
}else if (_enemyHp <= 10 && _enemyMp > 4)
|
||||
{
|
||||
_enemyHp += 4;
|
||||
_enemyMp -= 4;
|
||||
_latestEnemyActionMessage = Loc.GetString("{0} heals for 4 health!", _enemyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
var attackAmount = _random.Next(3, 6);
|
||||
_latestEnemyActionMessage =
|
||||
Loc.GetString("{0} attacks you for {1} damage!", _enemyName, attackAmount);
|
||||
if (_owner._playerInvincibilityFlag) return;
|
||||
_playerHp -= attackAmount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Metadata-message based on the objects values.
|
||||
/// </summary>
|
||||
/// <returns>A Metadata-message.</returns>
|
||||
public SpaceVillainArcadeMetaDataUpdateMessage GenerateMetaDataMessage()
|
||||
{
|
||||
return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage, _latestEnemyActionMessage, Name, _enemyName, !_running);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Update-message based on the objects values.
|
||||
/// </summary>
|
||||
/// <returns>An Update-Message.</returns>
|
||||
public SpaceVillainArcadeDataUpdateMessage
|
||||
GenerateUpdateMessage()
|
||||
{
|
||||
return new(_playerHp, _playerMp, _enemyHp, _enemyMp, _latestPlayerActionMessage,
|
||||
_latestEnemyActionMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#nullable enable
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Temperature;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.Interfaces.GameObjects;
|
||||
using Content.Server.Alert;
|
||||
using Content.Server.Pressure;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Atmos
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Body.Respiratory;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Server.Body.Respiratory;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Doors;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Doors;
|
||||
using Content.Shared.GameObjects.Components.Doors;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.Doors;
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Notification;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Atmos
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Alert;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Temperature;
|
||||
using Content.Server.Stunnable.Components;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.GameObjects.Components.Atmos;
|
||||
using Content.Shared.GameObjects.Components.Damage;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Notification;
|
||||
using Content.Shared.Temperature;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Server.Utility;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Components.Atmos;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Notification;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -5,11 +5,11 @@ using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Atmos.Piping;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Server.Utility;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.GameObjects.Components.Atmos;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
@@ -2,19 +2,22 @@
|
||||
#nullable disable warnings
|
||||
using System;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.Explosions;
|
||||
using Content.Server.GameObjects.Components.Body.Respiratory;
|
||||
using Content.Server.Body.Respiratory;
|
||||
using Content.Server.Explosion;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Server.Utility;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.DragDrop;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameObjects.Components.Atmos.GasTank;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
|
||||
@@ -7,9 +7,9 @@ using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Atmos.Piping;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.GameObjects.EntitySystems.Atmos;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.GameObjects.Components.Mobs.State;
|
||||
using Content.Shared.MobState;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.NodeContainer;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.NodeContainer;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.GameObjects.Components.Atmos;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.NodeContainer;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.NodeContainer;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.NodeContainer;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.GameObjects.Components.Atmos;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.NodeContainer;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.GameObjects.Components.Atmos;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.NodeContainer;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.GameObjects.Components.Atmos;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Interfaces.GameObjects;
|
||||
using Content.Server.Pressure;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Atmos.Piping;
|
||||
using Content.Server.GameObjects.Components.NodeContainer.NodeGroups;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.BarSign
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class BarSignComponent : Component, IMapInit
|
||||
{
|
||||
public override string Name => "BarSign";
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
|
||||
[DataField("current")]
|
||||
private string? _currentSign;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? CurrentSign
|
||||
{
|
||||
get => _currentSign;
|
||||
set
|
||||
{
|
||||
_currentSign = value;
|
||||
UpdateSignInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
|
||||
|
||||
private void UpdateSignInfo()
|
||||
{
|
||||
if (_currentSign == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_prototypeManager.TryIndex(_currentSign, out BarSignPrototype? prototype))
|
||||
{
|
||||
Logger.ErrorS("barSign", $"Invalid bar sign prototype: \"{_currentSign}\"");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
if (!Powered)
|
||||
{
|
||||
sprite.LayerSetState(0, "empty");
|
||||
sprite.LayerSetShader(0, "shaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.LayerSetState(0, prototype.Icon);
|
||||
sprite.LayerSetShader(0, "unshaded");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(prototype.Name))
|
||||
{
|
||||
Owner.Name = prototype.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.Name = Loc.GetString("bar sign");
|
||||
}
|
||||
|
||||
Owner.Description = prototype.Description;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdateSignInfo();
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
PowerOnOnPowerStateChanged(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void PowerOnOnPowerStateChanged(PowerChangedMessage e)
|
||||
{
|
||||
UpdateSignInfo();
|
||||
}
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
if (_currentSign != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var prototypes = _prototypeManager.EnumeratePrototypes<BarSignPrototype>().Where(p => !p.Hidden)
|
||||
.ToList();
|
||||
var prototype = _robustRandom.Pick(prototypes);
|
||||
|
||||
CurrentSign = prototype.ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#nullable enable
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.BarSign
|
||||
{
|
||||
[Prototype("barSign")]
|
||||
public class BarSignPrototype : IPrototype
|
||||
{
|
||||
private string _description = "";
|
||||
private string _name = "";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
|
||||
[DataField("icon")] public string Icon { get; private set; } = "";
|
||||
|
||||
[DataField("name")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
private set => _name = Loc.GetString(value);
|
||||
}
|
||||
|
||||
[DataField("description")]
|
||||
public string Description
|
||||
{
|
||||
get => _description;
|
||||
private set => _description = Loc.GetString(value);
|
||||
}
|
||||
|
||||
[DataField("renameArea")]
|
||||
public bool RenameArea { get; private set; } = true;
|
||||
[DataField("hidden")]
|
||||
public bool Hidden { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Observer;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public class BrainBehavior : MechanismBehavior
|
||||
{
|
||||
protected override void OnAddedToBody(IBody body)
|
||||
{
|
||||
base.OnAddedToBody(body);
|
||||
|
||||
HandleMind(body.Owner, Owner);
|
||||
}
|
||||
|
||||
protected override void OnAddedToPart(IBodyPart part)
|
||||
{
|
||||
base.OnAddedToPart(part);
|
||||
|
||||
HandleMind(part.Owner, Owner);
|
||||
}
|
||||
|
||||
protected override void OnAddedToPartInBody(IBody body, IBodyPart part)
|
||||
{
|
||||
base.OnAddedToPartInBody(body, part);
|
||||
|
||||
HandleMind(body.Owner, Owner);
|
||||
}
|
||||
|
||||
protected override void OnRemovedFromBody(IBody old)
|
||||
{
|
||||
base.OnRemovedFromBody(old);
|
||||
|
||||
HandleMind(Part!.Owner, old.Owner);
|
||||
}
|
||||
|
||||
protected override void OnRemovedFromPart(IBodyPart old)
|
||||
{
|
||||
base.OnRemovedFromPart(old);
|
||||
|
||||
HandleMind(Owner, old.Owner);
|
||||
}
|
||||
|
||||
protected override void OnRemovedFromPartInBody(IBody oldBody, IBodyPart oldPart)
|
||||
{
|
||||
base.OnRemovedFromPartInBody(oldBody, oldPart);
|
||||
|
||||
HandleMind(oldBody.Owner, Owner);
|
||||
}
|
||||
|
||||
private void HandleMind(IEntity newEntity, IEntity oldEntity)
|
||||
{
|
||||
newEntity.EnsureComponent<MindComponent>();
|
||||
var oldMind = oldEntity.EnsureComponent<MindComponent>();
|
||||
|
||||
if (!newEntity.HasComponent<IGhostOnMove>())
|
||||
newEntity.AddComponent<GhostOnMoveComponent>();
|
||||
|
||||
// TODO: This is an awful solution.
|
||||
if (!newEntity.HasComponent<IMoverComponent>())
|
||||
newEntity.AddComponent<SharedDummyInputMoverComponent>();
|
||||
|
||||
oldMind.Mind?.TransferTo(newEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Content.Shared.GameObjects.Components.Body.Networks;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public class HeartBehavior : MechanismBehavior
|
||||
{
|
||||
private float _accumulatedFrameTime;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
// TODO BODY do between pre and metabolism
|
||||
if (Parent.Body == null ||
|
||||
!Parent.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,111 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body.Networks;
|
||||
using Content.Shared.Interfaces.Chemistry;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Metabolizes reagents in <see cref="SharedBloodstreamComponent"/> after they are digested.
|
||||
/// </summary>
|
||||
public class LiverBehavior : MechanismBehavior
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private float _accumulatedFrameTime;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the liver is functional.
|
||||
/// </summary>
|
||||
//[ViewVariables] private bool _liverFailing = false;
|
||||
|
||||
/// <summary>
|
||||
/// Modifier for alcohol damage.
|
||||
/// </summary>
|
||||
//[DataField("alcoholLethality")]
|
||||
//[ViewVariables] private float _alcoholLethality = 0.005f;
|
||||
|
||||
/// <summary>
|
||||
/// Modifier for alcohol damage.
|
||||
/// </summary>
|
||||
//[DataField("alcoholExponent")]
|
||||
//[ViewVariables] private float _alcoholExponent = 1.6f;
|
||||
|
||||
/// <summary>
|
||||
/// Toxin volume that can be purged without damage.
|
||||
/// </summary>
|
||||
//[DataField("toxinTolerance")]
|
||||
//[ViewVariables] private float _toxinTolerance = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// Toxin damage modifier.
|
||||
/// </summary>
|
||||
//[DataField("toxinLethality")]
|
||||
//[ViewVariables] private float _toxinLethality = 0.01f;
|
||||
|
||||
/// <summary>
|
||||
/// Loops through each reagent in _internalSolution,
|
||||
/// and calls <see cref="IMetabolizable.Metabolize"/> for each of them.
|
||||
/// Also handles toxins and alcohol.
|
||||
/// </summary>
|
||||
/// <param name="frameTime">
|
||||
/// The time since the last update in seconds.
|
||||
/// </param>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulatedFrameTime += frameTime;
|
||||
|
||||
// Update at most once per second
|
||||
if (_accumulatedFrameTime < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulatedFrameTime -= 1;
|
||||
|
||||
if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (bloodstream.Solution.CurrentVolume <= ReagentUnit.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Run metabolism for each reagent, remove metabolized reagents
|
||||
// Using ToList here lets us edit reagents while iterating
|
||||
foreach (var reagent in bloodstream.Solution.ReagentList.ToList())
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? prototype))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//TODO BODY Check if it's a Toxin. If volume < _toxinTolerance, just remove it. If greater, add damage = volume * _toxinLethality
|
||||
//TODO BODY Check if it has BoozePower > 0. Affect drunkenness, apply damage. Proposed formula (SS13-derived): damage = sqrt(volume) * BoozePower^_alcoholExponent * _alcoholLethality / 10
|
||||
//TODO BODY Liver failure.
|
||||
|
||||
//TODO Make sure reagent prototypes actually have the toxin and boozepower vars set.
|
||||
|
||||
// Run metabolism code for each reagent
|
||||
foreach (var metabolizable in prototype.Metabolism)
|
||||
{
|
||||
var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, frameTime);
|
||||
bloodstream.Solution.TryRemoveReagent(reagent.ReagentId, reagentDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Atmos;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.GameObjects.Components.Body.Respiratory;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Mobs.State;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
[DataDefinition]
|
||||
public class LungBehavior : MechanismBehavior
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private float _accumulatedFrameTime;
|
||||
|
||||
[ViewVariables] private TimeSpan _lastGaspPopupTime;
|
||||
|
||||
[DataField("air")]
|
||||
[ViewVariables]
|
||||
public GasMixture Air { get; set; } = new()
|
||||
{
|
||||
Volume = 6,
|
||||
Temperature = Atmospherics.NormalBodyTemperature
|
||||
};
|
||||
|
||||
[DataField("gaspPopupCooldown")]
|
||||
[ViewVariables]
|
||||
public TimeSpan GaspPopupCooldown { get; private set; } = TimeSpan.FromSeconds(8);
|
||||
|
||||
[ViewVariables] public LungStatus Status { get; set; }
|
||||
|
||||
[DataField("cycleDelay")]
|
||||
[ViewVariables]
|
||||
public float CycleDelay { get; set; } = 2;
|
||||
|
||||
public LungBehavior()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void OnAddedToBody(IBody body)
|
||||
{
|
||||
base.OnAddedToBody(body);
|
||||
Inhale(CycleDelay);
|
||||
}
|
||||
|
||||
public void Gasp()
|
||||
{
|
||||
if (_gameTiming.CurTime >= _lastGaspPopupTime + GaspPopupCooldown)
|
||||
{
|
||||
_lastGaspPopupTime = _gameTiming.CurTime;
|
||||
Owner.PopupMessageEveryone(Loc.GetString("Gasp"));
|
||||
}
|
||||
|
||||
Inhale(CycleDelay);
|
||||
}
|
||||
|
||||
public void Transfer(GasMixture from, GasMixture to, float ratio)
|
||||
{
|
||||
to.Merge(from.RemoveRatio(ratio));
|
||||
}
|
||||
|
||||
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 (Body != null && Body.Owner.TryGetComponent(out IMobStateComponent? mobState) && mobState.IsCritical())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Status == LungStatus.None)
|
||||
{
|
||||
Status = LungStatus.Inhaling;
|
||||
}
|
||||
|
||||
_accumulatedFrameTime += Status switch
|
||||
{
|
||||
LungStatus.Inhaling => frameTime,
|
||||
LungStatus.Exhaling => -frameTime,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
var absoluteTime = Math.Abs(_accumulatedFrameTime);
|
||||
var delay = CycleDelay;
|
||||
|
||||
if (absoluteTime < delay)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (Status)
|
||||
{
|
||||
case LungStatus.Inhaling:
|
||||
Inhale(absoluteTime);
|
||||
Status = LungStatus.Exhaling;
|
||||
break;
|
||||
case LungStatus.Exhaling:
|
||||
Exhale(absoluteTime);
|
||||
Status = LungStatus.Inhaling;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
_accumulatedFrameTime = absoluteTime - delay;
|
||||
}
|
||||
|
||||
public void Inhale(float frameTime)
|
||||
{
|
||||
if (Body != null &&
|
||||
Body.Owner.TryGetComponent(out InternalsComponent? internals) &&
|
||||
internals.BreathToolEntity != null &&
|
||||
internals.GasTankEntity != null &&
|
||||
internals.BreathToolEntity.TryGetComponent(out BreathToolComponent? breathTool) &&
|
||||
breathTool.IsFunctional &&
|
||||
internals.GasTankEntity.TryGetComponent(out GasTankComponent? gasTank) &&
|
||||
gasTank.Air != null)
|
||||
{
|
||||
Inhale(frameTime, gasTank.RemoveAirVolume(Atmospherics.BreathVolume));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Inhale(frameTime, tileAir);
|
||||
}
|
||||
|
||||
public void Inhale(float frameTime, GasMixture from)
|
||||
{
|
||||
var ratio = (Atmospherics.BreathVolume / from.Volume) * frameTime;
|
||||
|
||||
Transfer(from, Air, ratio);
|
||||
ToBloodstream(Air);
|
||||
}
|
||||
|
||||
public void Exhale(float frameTime)
|
||||
{
|
||||
if (!Owner.Transform.Coordinates.TryGetTileAir(out var tileAir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Exhale(frameTime, tileAir);
|
||||
}
|
||||
|
||||
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 (Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bloodstream.PumpToxins(Air);
|
||||
|
||||
var lungRemoved = Air.RemoveRatio(0.5f);
|
||||
to.Merge(lungRemoved);
|
||||
}
|
||||
}
|
||||
|
||||
public enum LungStatus
|
||||
{
|
||||
None = 0,
|
||||
Inhaling,
|
||||
Exhaling
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Behavior;
|
||||
using Content.Shared.GameObjects.Components.Body.Mechanism;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public abstract class MechanismBehavior : IMechanismBehavior
|
||||
{
|
||||
public IBody? Body => Part?.Body;
|
||||
|
||||
public IBodyPart? Part => Parent.Part;
|
||||
|
||||
public IMechanism Parent { get; private set; } = default!;
|
||||
|
||||
public IEntity Owner => Parent.Owner;
|
||||
|
||||
public virtual void Initialize(IMechanism parent)
|
||||
{
|
||||
Parent = parent;
|
||||
}
|
||||
|
||||
public virtual void Startup()
|
||||
{
|
||||
if (Part == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Body == null)
|
||||
{
|
||||
AddedToPart(Part);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddedToPartInBody(Body, Part);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddedToBody(IBody body)
|
||||
{
|
||||
DebugTools.AssertNotNull(Body);
|
||||
DebugTools.AssertNotNull(body);
|
||||
|
||||
OnAddedToBody(body);
|
||||
}
|
||||
|
||||
public void AddedToPart(IBodyPart part)
|
||||
{
|
||||
DebugTools.AssertNotNull(Part);
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
OnAddedToPart(part);
|
||||
}
|
||||
|
||||
public void AddedToPartInBody(IBody body, IBodyPart part)
|
||||
{
|
||||
DebugTools.AssertNotNull(Body);
|
||||
DebugTools.AssertNotNull(body);
|
||||
DebugTools.AssertNotNull(Part);
|
||||
DebugTools.AssertNotNull(part);
|
||||
|
||||
OnAddedToPartInBody(body, part);
|
||||
}
|
||||
|
||||
public void RemovedFromBody(IBody old)
|
||||
{
|
||||
DebugTools.AssertNull(Body);
|
||||
DebugTools.AssertNotNull(old);
|
||||
|
||||
OnRemovedFromBody(old);
|
||||
}
|
||||
|
||||
public void RemovedFromPart(IBodyPart old)
|
||||
{
|
||||
DebugTools.AssertNull(Part);
|
||||
DebugTools.AssertNotNull(old);
|
||||
|
||||
OnRemovedFromPart(old);
|
||||
}
|
||||
|
||||
public void RemovedFromPartInBody(IBody oldBody, IBodyPart oldPart)
|
||||
{
|
||||
DebugTools.AssertNull(Body);
|
||||
DebugTools.AssertNull(Part);
|
||||
DebugTools.AssertNotNull(oldBody);
|
||||
DebugTools.AssertNotNull(oldPart);
|
||||
|
||||
OnRemovedFromPartInBody(oldBody, oldPart);
|
||||
}
|
||||
|
||||
protected virtual void OnAddedToBody(IBody body) { }
|
||||
|
||||
protected virtual void OnAddedToPart(IBodyPart part) { }
|
||||
|
||||
protected virtual void OnAddedToPartInBody(IBody body, IBodyPart part) { }
|
||||
|
||||
protected virtual void OnRemovedFromBody(IBody old) { }
|
||||
|
||||
protected virtual void OnRemovedFromPart(IBodyPart old) { }
|
||||
|
||||
protected virtual void OnRemovedFromPartInBody(IBody oldBody, IBodyPart oldPart) { }
|
||||
|
||||
public virtual void Update(float frameTime) { }
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Behavior;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
public static class MechanismExtensions
|
||||
{
|
||||
public static bool HasMechanismBehavior<T>(this IBody body) where T : IMechanismBehavior
|
||||
{
|
||||
return body.Parts.Any(p => p.Key.HasMechanismBehavior<T>());
|
||||
}
|
||||
|
||||
public static bool HasMechanismBehavior<T>(this IBodyPart part) where T : IMechanismBehavior
|
||||
{
|
||||
return part.Mechanisms.Any(m => m.HasBehavior<T>());
|
||||
}
|
||||
|
||||
public static IEnumerable<IMechanismBehavior> GetMechanismBehaviors(this IBody body)
|
||||
{
|
||||
foreach (var (part, _) in body.Parts)
|
||||
foreach (var mechanism in part.Mechanisms)
|
||||
foreach (var behavior in mechanism.Behaviors.Values)
|
||||
{
|
||||
yield return behavior;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetMechanismBehaviors(this IBody body,
|
||||
[NotNullWhen(true)] out List<IMechanismBehavior>? behaviors)
|
||||
{
|
||||
behaviors = body.GetMechanismBehaviors().ToList();
|
||||
|
||||
if (behaviors.Count == 0)
|
||||
{
|
||||
behaviors = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IEnumerable<T> GetMechanismBehaviors<T>(this IBody body) where T : class, IMechanismBehavior
|
||||
{
|
||||
foreach (var (part, _) in body.Parts)
|
||||
foreach (var mechanism in part.Mechanisms)
|
||||
foreach (var behavior in mechanism.Behaviors.Values)
|
||||
{
|
||||
if (behavior is T tBehavior)
|
||||
{
|
||||
yield return tBehavior;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetMechanismBehaviors<T>(this IBody entity, [NotNullWhen(true)] out List<T>? behaviors)
|
||||
where T : class, IMechanismBehavior
|
||||
{
|
||||
behaviors = entity.GetMechanismBehaviors<T>().ToList();
|
||||
|
||||
if (behaviors.Count == 0)
|
||||
{
|
||||
behaviors = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +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.Body.Networks;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Behavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Where reagents go when ingested. Tracks ingested reagents over time, and
|
||||
/// eventually transfers them to <see cref="SharedBloodstreamComponent"/> once digested.
|
||||
/// </summary>
|
||||
public class StomachBehavior : MechanismBehavior
|
||||
{
|
||||
private float _accumulatedFrameTime;
|
||||
|
||||
/// <summary>
|
||||
/// Updates digestion status of ingested reagents.
|
||||
/// Once reagents surpass _digestionDelay they are moved to the
|
||||
/// bloodstream, where they are then metabolized.
|
||||
/// </summary>
|
||||
/// <param name="frameTime">
|
||||
/// The time since the last update in seconds.
|
||||
/// </param>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
if (Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulatedFrameTime += frameTime;
|
||||
|
||||
// Update at most once per second
|
||||
if (_accumulatedFrameTime < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_accumulatedFrameTime -= 1;
|
||||
|
||||
if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solution) ||
|
||||
!Body.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(1);
|
||||
if (delta.Lifetime > _digestionDelay)
|
||||
{
|
||||
solution.TryRemoveReagent(delta.ReagentId, delta.Quantity);
|
||||
transferSolution.AddReagent(delta.ReagentId, delta.Quantity);
|
||||
_reagentDeltas.Remove(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer digested reagents to bloodstream
|
||||
bloodstream.TryTransferSolution(transferSolution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Max volume of internal solution storage
|
||||
/// </summary>
|
||||
public ReagentUnit MaxVolume
|
||||
{
|
||||
get => Owner.TryGetComponent(out SharedSolutionContainerComponent? solution) ? solution.MaxVolume : ReagentUnit.Zero;
|
||||
set
|
||||
{
|
||||
if (Owner.TryGetComponent(out SharedSolutionContainerComponent? solution))
|
||||
{
|
||||
solution.MaxVolume = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initial internal solution storage volume
|
||||
/// </summary>
|
||||
[DataField("maxVolume")]
|
||||
[ViewVariables]
|
||||
protected ReagentUnit InitialMaxVolume { get; private set; } = ReagentUnit.New(100);
|
||||
|
||||
/// <summary>
|
||||
/// Time in seconds between reagents being ingested and them being
|
||||
/// transferred to <see cref="SharedBloodstreamComponent"/>
|
||||
/// </summary>
|
||||
[DataField("digestionDelay")] [ViewVariables]
|
||||
private float _digestionDelay = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Used to track how long each reagent has been in the stomach
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private readonly List<ReagentDelta> _reagentDeltas = new();
|
||||
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
Owner.EnsureComponentWarn(out SolutionContainerComponent solution);
|
||||
|
||||
solution.MaxVolume = InitialMaxVolume;
|
||||
}
|
||||
|
||||
public bool CanTransferSolution(Solution solution)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SharedSolutionContainerComponent? solutionComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: For now no partial transfers. Potentially change by design
|
||||
if (!solutionComponent.CanAddSolution(solution))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTransferSolution(Solution solution)
|
||||
{
|
||||
if (Body == null || !CanTransferSolution(solution))
|
||||
return false;
|
||||
|
||||
if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add solution to _stomachContents
|
||||
solutionComponent.TryAddSolution(solution);
|
||||
// Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
|
||||
foreach (var reagent in solution.Contents)
|
||||
{
|
||||
_reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to track quantity changes when ingesting & digesting reagents
|
||||
/// </summary>
|
||||
protected class ReagentDelta
|
||||
{
|
||||
public readonly string ReagentId;
|
||||
public readonly ReagentUnit Quantity;
|
||||
public float Lifetime { get; private set; }
|
||||
|
||||
public ReagentDelta(string reagentId, ReagentUnit quantity)
|
||||
{
|
||||
ReagentId = reagentId;
|
||||
Quantity = quantity;
|
||||
Lifetime = 0.0f;
|
||||
}
|
||||
|
||||
public void Increment(float delta) => Lifetime += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Server.Commands.Observer;
|
||||
using Content.Server.GameObjects.Components.Observer;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.Interfaces.GameTicking;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Body.Slot;
|
||||
using Content.Shared.GameObjects.Components.Mobs.State;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedBodyComponent))]
|
||||
[ComponentReference(typeof(IBody))]
|
||||
[ComponentReference(typeof(IGhostOnMove))]
|
||||
public class BodyComponent : SharedBodyComponent, IRelayMoveInput, IGhostOnMove
|
||||
{
|
||||
private Container _partContainer = default!;
|
||||
[Dependency] private readonly IGameTicker _gameTicker = default!;
|
||||
|
||||
protected override bool CanAddPart(string slotId, IBodyPart part)
|
||||
{
|
||||
return base.CanAddPart(slotId, part) &&
|
||||
_partContainer.CanInsert(part.Owner);
|
||||
}
|
||||
|
||||
protected override void OnAddPart(BodyPartSlot slot, IBodyPart part)
|
||||
{
|
||||
base.OnAddPart(slot, part);
|
||||
|
||||
_partContainer.Insert(part.Owner);
|
||||
}
|
||||
|
||||
protected override void OnRemovePart(BodyPartSlot slot, IBodyPart part)
|
||||
{
|
||||
base.OnRemovePart(slot, part);
|
||||
|
||||
_partContainer.ForceRemove(part.Owner);
|
||||
part.Owner.RandomOffset(0.25f);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_partContainer = Owner.EnsureContainer<Container>($"{Name}-{nameof(BodyComponent)}");
|
||||
var preset = Preset;
|
||||
|
||||
if (preset != null)
|
||||
{
|
||||
foreach (var slot in Slots)
|
||||
{
|
||||
// Using MapPosition instead of Coordinates here prevents
|
||||
// a crash within the character preview menu in the lobby
|
||||
var entity = Owner.EntityManager.SpawnEntity(preset.PartIDs[slot.Id], Owner.Transform.MapPosition);
|
||||
|
||||
if (!entity.TryGetComponent(out IBodyPart? part))
|
||||
{
|
||||
Logger.Error($"Entity {slot.Id} does not have a {nameof(IBodyPart)} component.");
|
||||
continue;
|
||||
}
|
||||
|
||||
SetPart(slot.Id, part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
part.Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void IRelayMoveInput.MoveInputPressed(ICommonSession session)
|
||||
{
|
||||
if (Owner.TryGetComponent(out IMobStateComponent? mobState) &&
|
||||
mobState.IsDead() &&
|
||||
Owner.TryGetComponent(out MindComponent? mind) &&
|
||||
mind.HasMind)
|
||||
{
|
||||
_gameTicker.OnGhostAttempt(mind.Mind!, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Gib(bool gibParts = false)
|
||||
{
|
||||
base.Gib(gibParts);
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(Owner), AudioHelpers.GetRandomFileFromSoundCollection("gib"), Owner.Transform.Coordinates,
|
||||
AudioHelpers.WithVariation(0.025f));
|
||||
|
||||
if (Owner.TryGetComponent(out ContainerManagerComponent? container))
|
||||
{
|
||||
foreach (var cont in container.GetAllContainers())
|
||||
{
|
||||
foreach (var ent in cont.ContainedEntities)
|
||||
{
|
||||
cont.ForceRemove(ent);
|
||||
ent.Transform.Coordinates = Owner.Transform.Coordinates;
|
||||
ent.RandomOffset(0.25f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Owner.QueueDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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 BodyDamageChangeParams : DamageChangeParams, IBodyHealthChangeParams
|
||||
{
|
||||
public BodyDamageChangeParams(BodyPartType part)
|
||||
{
|
||||
Part = part;
|
||||
}
|
||||
|
||||
public BodyPartType Part { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.GameObjects.Components.Body.Scanner;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
[ComponentReference(typeof(SharedBodyScannerComponent))]
|
||||
public class BodyScannerComponent : SharedBodyScannerComponent, IActivate
|
||||
{
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BodyScannerUiKey.Key);
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var session = actor.PlayerSession;
|
||||
|
||||
if (session.AttachedEntity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.AttachedEntity.TryGetComponent(out IBody? body))
|
||||
{
|
||||
var state = InterfaceState(body);
|
||||
UserInterface?.SetState(state);
|
||||
}
|
||||
|
||||
UserInterface?.Open(session);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Owner.EnsureComponentWarn<ServerUserInterfaceComponent>();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) { }
|
||||
|
||||
/// <summary>
|
||||
/// Copy BodyTemplate and BodyPart data into a common data class that the client can read.
|
||||
/// </summary>
|
||||
private BodyScannerUIState InterfaceState(IBody body)
|
||||
{
|
||||
return new(body.Owner.Uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
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.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Circulatory
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedBloodstreamComponent))]
|
||||
public class BloodstreamComponent : SharedBloodstreamComponent, IGasMixtureHolder
|
||||
{
|
||||
public override string Name => "Bloodstream";
|
||||
|
||||
/// <summary>
|
||||
/// Max volume of internal solution storage
|
||||
/// </summary>
|
||||
[DataField("maxVolume")]
|
||||
[ViewVariables] private ReagentUnit _initialMaxVolume = ReagentUnit.New(250);
|
||||
|
||||
/// <summary>
|
||||
/// Internal solution for reagent storage
|
||||
/// </summary>
|
||||
[ViewVariables] private SolutionContainerComponent _internalSolution = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Empty volume of internal solution
|
||||
/// </summary>
|
||||
[ViewVariables] public ReagentUnit EmptyVolume => _internalSolution.EmptyVolume;
|
||||
|
||||
[ViewVariables]
|
||||
public GasMixture Air { get; set; } = new(6)
|
||||
{Temperature = Atmospherics.NormalBodyTemperature};
|
||||
|
||||
[ViewVariables] public SolutionContainerComponent Solution => _internalSolution;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_internalSolution = Owner.EnsureComponent<SolutionContainerComponent>();
|
||||
_internalSolution.MaxVolume = _initialMaxVolume;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to transfer provided solution to internal solution.
|
||||
/// Only supports complete transfers
|
||||
/// </summary>
|
||||
/// <param name="solution">Solution to be transferred</param>
|
||||
/// <returns>Whether or not transfer was a success</returns>
|
||||
public override bool TryTransferSolution(Solution solution)
|
||||
{
|
||||
// For now doesn't support partial transfers
|
||||
if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_internalSolution.TryAddSolution(solution);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void PumpToxins(GasMixture to)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out MetabolismComponent? metabolism))
|
||||
{
|
||||
to.Merge(Air);
|
||||
Air.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var toxins = metabolism.Clean(this);
|
||||
var toOld = to.Gases.ToArray();
|
||||
|
||||
to.Merge(toxins);
|
||||
|
||||
for (var i = 0; i < toOld.Length; i++)
|
||||
{
|
||||
var newAmount = to.GetMoles(i);
|
||||
var oldAmount = toOld[i];
|
||||
var delta = newAmount - oldAmount;
|
||||
|
||||
toxins.AdjustMoles(i, -delta);
|
||||
}
|
||||
|
||||
Air.Merge(toxins);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseAllSurgeryUIs();
|
||||
OptionsCache.Clear();
|
||||
PerformerCache = null;
|
||||
BodyCache = null;
|
||||
|
||||
if (eventArgs.Target.TryGetComponent(out IBody? 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!"));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 (part, slot) in body.Parts)
|
||||
{
|
||||
// For each limb in the target, add it to our cache if it is a valid option.
|
||||
if (part.CanAddMechanism(this))
|
||||
{
|
||||
OptionsCache.Add(IdHash, slot);
|
||||
toSend.Add(part + ": " + part.Name, IdHash++);
|
||||
}
|
||||
}
|
||||
|
||||
if (OptionsCache.Count > 0 &&
|
||||
eventArgs.User.TryGetComponent(out ActorComponent? 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 ActorComponent? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
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.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Part
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedBodyPartComponent))]
|
||||
[ComponentReference(typeof(IBodyPart))]
|
||||
public class BodyPartComponent : SharedBodyPartComponent, IAfterInteract
|
||||
{
|
||||
private readonly Dictionary<int, object> _optionsCache = new();
|
||||
private IBody? _owningBodyCache;
|
||||
private int _idHash;
|
||||
private IEntity? _surgeonCache;
|
||||
private Container _mechanismContainer = default!;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
|
||||
|
||||
public override bool CanAddMechanism(IMechanism mechanism)
|
||||
{
|
||||
return base.CanAddMechanism(mechanism) &&
|
||||
_mechanismContainer.CanInsert(mechanism.Owner);
|
||||
}
|
||||
|
||||
protected override void OnAddMechanism(IMechanism mechanism)
|
||||
{
|
||||
base.OnAddMechanism(mechanism);
|
||||
|
||||
_mechanismContainer.Insert(mechanism.Owner);
|
||||
}
|
||||
|
||||
protected override void OnRemoveMechanism(IMechanism mechanism)
|
||||
{
|
||||
base.OnRemoveMechanism(mechanism);
|
||||
|
||||
_mechanismContainer.Remove(mechanism.Owner);
|
||||
mechanism.Owner.RandomOffset(0.25f);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_mechanismContainer = Owner.EnsureContainer<Container>($"{Name}-{nameof(BodyPartComponent)}");
|
||||
|
||||
// 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 += OnUIMessage;
|
||||
}
|
||||
|
||||
foreach (var mechanism in Mechanisms)
|
||||
{
|
||||
mechanism.Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
// TODO BODY
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseAllSurgeryUIs();
|
||||
_optionsCache.Clear();
|
||||
_surgeonCache = null;
|
||||
_owningBodyCache = null;
|
||||
|
||||
if (eventArgs.Target.TryGetComponent(out IBody? body))
|
||||
{
|
||||
SendSlots(eventArgs, body);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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.
|
||||
foreach (var slot in body.EmptySlots)
|
||||
{
|
||||
if (slot.PartType != PartType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var connection in slot.Connections)
|
||||
{
|
||||
if (connection.Part == null ||
|
||||
!connection.Part.CanAttachPart(this))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_optionsCache.Add(_idHash, slot);
|
||||
toSend.Add(slot.Id, _idHash++);
|
||||
}
|
||||
}
|
||||
|
||||
if (_optionsCache.Count > 0)
|
||||
{
|
||||
OpenSurgeryUI(eventArgs.User.GetComponent<ActorComponent>().PlayerSession);
|
||||
BodyPartSlotRequest(eventArgs.User.GetComponent<ActorComponent>().PlayerSession,
|
||||
toSend);
|
||||
_surgeonCache = eventArgs.User;
|
||||
_owningBodyCache = body;
|
||||
}
|
||||
else // If surgery cannot be performed, show message saying so.
|
||||
{
|
||||
eventArgs.Target?.PopupMessage(eventArgs.User,
|
||||
Loc.GetString("You see no way to install {0:theName}.", Owner));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the client chooses from a list of possible
|
||||
/// BodyPartSlots to install the limb on.
|
||||
/// </summary>
|
||||
private void ReceiveBodyPartSlot(int key)
|
||||
{
|
||||
if (_surgeonCache == null ||
|
||||
!_surgeonCache.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseSurgeryUI(actor.PlayerSession);
|
||||
|
||||
if (_owningBodyCache == 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))
|
||||
{
|
||||
_owningBodyCache.Owner.PopupMessage(_surgeonCache,
|
||||
Loc.GetString("You see no useful way to attach {0:theName} anymore.", Owner));
|
||||
}
|
||||
|
||||
var target = (string) targetObject!;
|
||||
var message = _owningBodyCache.TryAddPart(target, this)
|
||||
? Loc.GetString("You attach {0:theName}.", Owner)
|
||||
: Loc.GetString("You can't attach {0:theName}!", Owner);
|
||||
|
||||
_owningBodyCache.Owner.PopupMessage(_surgeonCache, message);
|
||||
}
|
||||
|
||||
private void OpenSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Open(session);
|
||||
}
|
||||
|
||||
private void BodyPartSlotRequest(IPlayerSession session, Dictionary<string, int> options)
|
||||
{
|
||||
UserInterface?.SendMessage(new RequestBodyPartSlotSurgeryUIMessage(options), session);
|
||||
}
|
||||
|
||||
private void CloseSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Close(session);
|
||||
}
|
||||
|
||||
private void CloseAllSurgeryUIs()
|
||||
{
|
||||
UserInterface?.CloseAll();
|
||||
}
|
||||
|
||||
private void OnUIMessage(ServerBoundUserInterfaceMessage message)
|
||||
{
|
||||
switch (message.Message)
|
||||
{
|
||||
case ReceiveBodyPartSlotSurgeryUIMessage msg:
|
||||
ReceiveBodyPartSlot(msg.SelectedOptionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
public class AttachBodyPartVerb : Verb<BodyPartComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BodyPartComponent component, VerbData data)
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
|
||||
if (user == component.Owner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var groupController = IoCManager.Resolve<IConGroupController>();
|
||||
|
||||
if (!groupController.CanCommand(actor.PlayerSession, "attachbodypart"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user.TryGetComponent(out IBody? body))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.HasPart(component))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
data.Visibility = VerbVisibility.Visible;
|
||||
data.Text = Loc.GetString("Attach Body Part");
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BodyPartComponent component)
|
||||
{
|
||||
if (!user.TryGetComponent(out IBody? body))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
body.SetPart($"{nameof(AttachBodyPartVerb)}-{component.Owner.Uid}", component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Atmos;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Respiratory
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class InternalsComponent : Component
|
||||
{
|
||||
public override string Name => "Internals";
|
||||
[ViewVariables] public IEntity? GasTankEntity { get; set; }
|
||||
[ViewVariables] public IEntity? BreathToolEntity { get; set; }
|
||||
|
||||
public void DisconnectBreathTool()
|
||||
{
|
||||
var old = BreathToolEntity;
|
||||
BreathToolEntity = null;
|
||||
|
||||
if (old != null && old.TryGetComponent(out BreathToolComponent? breathTool) )
|
||||
{
|
||||
breathTool.DisconnectInternals();
|
||||
DisconnectTank();
|
||||
}
|
||||
}
|
||||
|
||||
public void ConnectBreathTool(IEntity toolEntity)
|
||||
{
|
||||
if (BreathToolEntity != null && BreathToolEntity.TryGetComponent(out BreathToolComponent? tool))
|
||||
{
|
||||
tool.DisconnectInternals();
|
||||
}
|
||||
|
||||
BreathToolEntity = toolEntity;
|
||||
}
|
||||
|
||||
public void DisconnectTank()
|
||||
{
|
||||
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
|
||||
{
|
||||
tank.DisconnectFromInternals(Owner);
|
||||
}
|
||||
|
||||
GasTankEntity = null;
|
||||
}
|
||||
|
||||
public bool TryConnectTank(IEntity tankEntity)
|
||||
{
|
||||
if (BreathToolEntity == null)
|
||||
return false;
|
||||
|
||||
if (GasTankEntity != null && GasTankEntity.TryGetComponent(out GasTankComponent? tank))
|
||||
{
|
||||
tank.DisconnectFromInternals(Owner);
|
||||
}
|
||||
|
||||
GasTankEntity = tankEntity;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AreInternalsWorking()
|
||||
{
|
||||
return BreathToolEntity != null &&
|
||||
GasTankEntity != null &&
|
||||
BreathToolEntity.TryGetComponent(out BreathToolComponent? breathTool) &&
|
||||
breathTool.IsFunctional &&
|
||||
GasTankEntity.TryGetComponent(out GasTankComponent? gasTank) &&
|
||||
gasTank.Air != null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
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 Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using static Content.Shared.GameObjects.Components.Body.Surgery.ISurgeryData;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
/// <summary>
|
||||
/// Data class representing the surgery state of a biological entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(ISurgeryData))]
|
||||
public class BiologicalSurgeryDataComponent : Component, ISurgeryData
|
||||
{
|
||||
public override string Name => "BiologicalSurgeryData";
|
||||
|
||||
private readonly HashSet<IMechanism> _disconnectedOrgans = new();
|
||||
|
||||
private bool SkinOpened { get; set; }
|
||||
|
||||
private bool SkinRetracted { get; set; }
|
||||
|
||||
private bool VesselsClamped { get; set; }
|
||||
|
||||
public IBodyPart? Parent => Owner.GetComponentOrNull<IBodyPart>();
|
||||
|
||||
public BodyPartType? ParentType => Parent?.PartType;
|
||||
|
||||
private void AddDisconnectedOrgan(IMechanism mechanism)
|
||||
{
|
||||
if (_disconnectedOrgans.Add(mechanism))
|
||||
{
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDisconnectedOrgan(IMechanism mechanism)
|
||||
{
|
||||
if (_disconnectedOrgans.Remove(mechanism))
|
||||
{
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> SurgeryDoAfter(IEntity performer)
|
||||
{
|
||||
if (!performer.HasComponent<DoAfterComponent>())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
||||
var target = Parent?.Body?.Owner ?? Owner;
|
||||
var args = new DoAfterEventArgs(performer, 3, target: target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnTargetMove = true
|
||||
};
|
||||
|
||||
return await doAfterSystem.DoAfter(args) == DoAfterStatus.Finished;
|
||||
}
|
||||
|
||||
private bool HasIncisionNotClamped()
|
||||
{
|
||||
return SkinOpened && !VesselsClamped;
|
||||
}
|
||||
|
||||
private bool HasClampedIncisionNotRetracted()
|
||||
{
|
||||
return SkinOpened && VesselsClamped && !SkinRetracted;
|
||||
}
|
||||
|
||||
private bool HasFullyOpenIncision()
|
||||
{
|
||||
return SkinOpened && VesselsClamped && SkinRetracted;
|
||||
}
|
||||
|
||||
public string GetDescription()
|
||||
{
|
||||
if (Parent == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var toReturn = new StringBuilder();
|
||||
|
||||
if (HasIncisionNotClamped())
|
||||
{
|
||||
toReturn.Append(Loc.GetString("The skin on {0:their} {1} has an incision, but it is prone to bleeding.",
|
||||
Owner, Parent.Name));
|
||||
}
|
||||
else if (HasClampedIncisionNotRetracted())
|
||||
{
|
||||
toReturn.AppendLine(Loc.GetString("The skin on {0:their} {1} has an incision, but it is not retracted.",
|
||||
Owner, Parent.Name));
|
||||
}
|
||||
else if (HasFullyOpenIncision())
|
||||
{
|
||||
toReturn.AppendLine(Loc.GetString("There is an incision on {0:their} {1}.\n", Owner, Parent.Name));
|
||||
foreach (var mechanism in _disconnectedOrgans)
|
||||
{
|
||||
toReturn.AppendLine(Loc.GetString("{0:their} {1} is loose.", Owner, mechanism.Name));
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn.ToString();
|
||||
}
|
||||
|
||||
public bool CanAddMechanism(IMechanism mechanism)
|
||||
{
|
||||
return Parent != null &&
|
||||
SkinOpened &&
|
||||
VesselsClamped &&
|
||||
SkinRetracted;
|
||||
}
|
||||
|
||||
public bool CanAttachBodyPart(IBodyPart part)
|
||||
{
|
||||
return Parent != null;
|
||||
// TODO BODY if a part is disconnected, you should have to do some surgery to allow another body part to be attached.
|
||||
}
|
||||
|
||||
public SurgeryAction? GetSurgeryStep(SurgeryType toolType)
|
||||
{
|
||||
if (Parent == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (toolType == SurgeryType.Amputation)
|
||||
{
|
||||
return RemoveBodyPartSurgery;
|
||||
}
|
||||
|
||||
if (!SkinOpened)
|
||||
{
|
||||
// Case: skin is normal.
|
||||
if (toolType == SurgeryType.Incision)
|
||||
{
|
||||
return OpenSkinSurgery;
|
||||
}
|
||||
}
|
||||
else if (!VesselsClamped)
|
||||
{
|
||||
// Case: skin is opened, but not clamped.
|
||||
switch (toolType)
|
||||
{
|
||||
case SurgeryType.VesselCompression:
|
||||
return ClampVesselsSurgery;
|
||||
case SurgeryType.Cauterization:
|
||||
return CauterizeIncisionSurgery;
|
||||
}
|
||||
}
|
||||
else if (!SkinRetracted)
|
||||
{
|
||||
// Case: skin is opened and clamped, but not retracted.
|
||||
switch (toolType)
|
||||
{
|
||||
case SurgeryType.Retraction:
|
||||
return RetractSkinSurgery;
|
||||
case SurgeryType.Cauterization:
|
||||
return CauterizeIncisionSurgery;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case: skin is fully open.
|
||||
if (Parent.Mechanisms.Count > 0 &&
|
||||
toolType == SurgeryType.VesselCompression)
|
||||
{
|
||||
if (_disconnectedOrgans.Except(Parent.Mechanisms).Count() != 0 ||
|
||||
Parent.Mechanisms.Except(_disconnectedOrgans).Count() != 0)
|
||||
{
|
||||
return LoosenOrganSurgery;
|
||||
}
|
||||
}
|
||||
|
||||
if (_disconnectedOrgans.Count > 0 && toolType == SurgeryType.Incision)
|
||||
{
|
||||
return RemoveOrganSurgery;
|
||||
}
|
||||
|
||||
if (toolType == SurgeryType.Cauterization)
|
||||
{
|
||||
return CauterizeIncisionSurgery;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool CheckSurgery(SurgeryType toolType)
|
||||
{
|
||||
return GetSurgeryStep(toolType) != null;
|
||||
}
|
||||
|
||||
public bool PerformSurgery(SurgeryType surgeryType, IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
var step = GetSurgeryStep(surgeryType);
|
||||
|
||||
if (step == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
step(container, surgeon, performer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void OpenSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Cut open the skin..."));
|
||||
|
||||
if (await SurgeryDoAfter(performer))
|
||||
{
|
||||
SkinOpened = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ClampVesselsSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Clamp the vessels..."));
|
||||
|
||||
if (await SurgeryDoAfter(performer))
|
||||
{
|
||||
VesselsClamped = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void RetractSkinSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Retracting the skin..."));
|
||||
|
||||
if (await SurgeryDoAfter(performer))
|
||||
{
|
||||
SkinRetracted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void CauterizeIncisionSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Cauterizing the incision..."));
|
||||
|
||||
if (await SurgeryDoAfter(performer))
|
||||
{
|
||||
SkinOpened = false;
|
||||
VesselsClamped = false;
|
||||
SkinRetracted = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoosenOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
if (Parent.Mechanisms.Count <= 0) return;
|
||||
|
||||
var toSend = new List<IMechanism>();
|
||||
foreach (var mechanism in Parent.Mechanisms)
|
||||
{
|
||||
if (!_disconnectedOrgans.Contains(mechanism))
|
||||
{
|
||||
toSend.Add(mechanism);
|
||||
}
|
||||
}
|
||||
|
||||
if (toSend.Count > 0)
|
||||
{
|
||||
surgeon.RequestMechanism(toSend, LoosenOrganSurgeryCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private async void LoosenOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon,
|
||||
IEntity performer)
|
||||
{
|
||||
if (Parent == null || target == null || !Parent.Mechanisms.Contains(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Loosening the organ..."));
|
||||
|
||||
if (!performer.HasComponent<DoAfterComponent>())
|
||||
{
|
||||
AddDisconnectedOrgan(target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await SurgeryDoAfter(performer))
|
||||
{
|
||||
AddDisconnectedOrgan(target);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOrganSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
|
||||
if (_disconnectedOrgans.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_disconnectedOrgans.Count == 1)
|
||||
{
|
||||
RemoveOrganSurgeryCallback(_disconnectedOrgans.First(), container, surgeon, performer);
|
||||
}
|
||||
else
|
||||
{
|
||||
surgeon.RequestMechanism(_disconnectedOrgans, RemoveOrganSurgeryCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RemoveOrganSurgeryCallback(IMechanism? target, IBodyPartContainer container, ISurgeon surgeon,
|
||||
IEntity performer)
|
||||
{
|
||||
if (Parent == null || target == null || !Parent.Mechanisms.Contains(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Removing the organ..."));
|
||||
|
||||
if (!performer.HasComponent<DoAfterComponent>())
|
||||
{
|
||||
Parent.RemoveMechanism(target, performer.Transform.Coordinates);
|
||||
RemoveDisconnectedOrgan(target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await SurgeryDoAfter(performer))
|
||||
{
|
||||
Parent.RemoveMechanism(target, performer.Transform.Coordinates);
|
||||
RemoveDisconnectedOrgan(target);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RemoveBodyPartSurgery(IBodyPartContainer container, ISurgeon surgeon, IEntity performer)
|
||||
{
|
||||
if (Parent == null) return;
|
||||
if (container is not IBody body) return;
|
||||
|
||||
performer.PopupMessage(Loc.GetString("Sawing off the limb!"));
|
||||
|
||||
if (await SurgeryDoAfter(performer))
|
||||
{
|
||||
body.RemovePart(Parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Content.Server.GameObjects.Components.Body.Surgery.Messages
|
||||
{
|
||||
public class SurgeryWindowCloseMessage
|
||||
{
|
||||
public SurgeryWindowCloseMessage(SurgeryToolComponent tool)
|
||||
{
|
||||
Tool = tool;
|
||||
}
|
||||
|
||||
public SurgeryToolComponent Tool { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Surgery.Messages
|
||||
{
|
||||
public class SurgeryWindowOpenMessage : ComponentMessage
|
||||
{
|
||||
public SurgeryWindowOpenMessage(SurgeryToolComponent tool)
|
||||
{
|
||||
Tool = tool;
|
||||
}
|
||||
|
||||
public SurgeryToolComponent Tool { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Body.Surgery.Messages;
|
||||
using Content.Server.Utility;
|
||||
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;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Body.Surgery
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-side component representing a generic tool capable of performing surgery.
|
||||
/// For instance, the scalpel.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class SurgeryToolComponent : Component, ISurgeon, IAfterInteract
|
||||
{
|
||||
public override string Name => "SurgeryTool";
|
||||
public override uint? NetID => ContentNetIDs.SURGERY;
|
||||
|
||||
private readonly Dictionary<int, object> _optionsCache = new();
|
||||
|
||||
[DataField("baseOperateTime")]
|
||||
private float _baseOperateTime = 5;
|
||||
|
||||
private ISurgeon.MechanismRequestCallback? _callbackCache;
|
||||
|
||||
private int _idHash;
|
||||
|
||||
[DataField("surgeryType")]
|
||||
private SurgeryType _surgeryType = SurgeryType.Incision;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SurgeryUIKey.Key);
|
||||
|
||||
public IBody? BodyCache { get; private set; }
|
||||
|
||||
public IEntity? PerformerCache { get; private set; }
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseAllSurgeryUIs();
|
||||
|
||||
// Attempt surgery on a body by sending a list of operable parts for the client to choose from
|
||||
if (eventArgs.Target.TryGetComponent(out 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 (part, slot) in body.Parts)
|
||||
{
|
||||
// For each limb in the target, add it to our cache if it is a valid option.
|
||||
if (part.SurgeryCheck(_surgeryType))
|
||||
{
|
||||
_optionsCache.Add(_idHash, part);
|
||||
toSend.Add(slot.Id + ": " + part.Name, _idHash++);
|
||||
}
|
||||
}
|
||||
|
||||
if (_optionsCache.Count > 0)
|
||||
{
|
||||
OpenSurgeryUI(actor.PlayerSession);
|
||||
UpdateSurgeryUIBodyPartRequest(actor.PlayerSession, toSend);
|
||||
PerformerCache = eventArgs.User; // Also, cache the data.
|
||||
BodyCache = body;
|
||||
}
|
||||
else // If surgery cannot be performed, show message saying so.
|
||||
{
|
||||
NotUsefulPopup();
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
// If surgery can be performed...
|
||||
if (!part.SurgeryCheck(_surgeryType))
|
||||
{
|
||||
NotUsefulPopup();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ...do the surgery.
|
||||
if (part.AttemptSurgery(_surgeryType, part, this,
|
||||
eventArgs.User))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Log error if the surgery fails somehow.
|
||||
Logger.Debug($"Error when trying to perform surgery on ${nameof(IBodyPart)} {eventArgs.User.Name}");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public float BaseOperationTime { get => _baseOperateTime; set => _baseOperateTime = value; }
|
||||
|
||||
public void RequestMechanism(IEnumerable<IMechanism> options, ISurgeon.MechanismRequestCallback callback)
|
||||
{
|
||||
var toSend = new Dictionary<string, int>();
|
||||
foreach (var mechanism in options)
|
||||
{
|
||||
_optionsCache.Add(_idHash, mechanism);
|
||||
toSend.Add(mechanism.Name, _idHash++);
|
||||
}
|
||||
|
||||
if (_optionsCache.Count > 0 && PerformerCache != null)
|
||||
{
|
||||
OpenSurgeryUI(PerformerCache.GetComponent<ActorComponent>().PlayerSession);
|
||||
UpdateSurgeryUIMechanismRequest(PerformerCache.GetComponent<ActorComponent>().PlayerSession,
|
||||
toSend);
|
||||
_callbackCache = callback;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Debug("Error on callback from mechanisms: there were no viable options to choose from!");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Open(session);
|
||||
|
||||
var message = new SurgeryWindowOpenMessage(this);
|
||||
|
||||
SendMessage(message);
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
|
||||
}
|
||||
|
||||
private void UpdateSurgeryUIBodyPartRequest(IPlayerSession session, Dictionary<string, int> options)
|
||||
{
|
||||
UserInterface?.SendMessage(new RequestBodyPartSurgeryUIMessage(options), session);
|
||||
}
|
||||
|
||||
private void UpdateSurgeryUIMechanismRequest(IPlayerSession session, Dictionary<string, int> options)
|
||||
{
|
||||
UserInterface?.SendMessage(new RequestMechanismSurgeryUIMessage(options), session);
|
||||
}
|
||||
|
||||
private void ClearUIData()
|
||||
{
|
||||
_optionsCache.Clear();
|
||||
|
||||
PerformerCache = null;
|
||||
BodyCache = null;
|
||||
_callbackCache = null;
|
||||
}
|
||||
|
||||
private void CloseSurgeryUI(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Close(session);
|
||||
ClearUIData();
|
||||
}
|
||||
|
||||
public void CloseAllSurgeryUIs()
|
||||
{
|
||||
UserInterface?.CloseAll();
|
||||
ClearUIData();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
|
||||
{
|
||||
switch (message.Message)
|
||||
{
|
||||
case ReceiveBodyPartSurgeryUIMessage msg:
|
||||
HandleReceiveBodyPart(msg.SelectedOptionId);
|
||||
break;
|
||||
case ReceiveMechanismSurgeryUIMessage msg:
|
||||
HandleReceiveMechanism(msg.SelectedOptionId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the client chooses from a list of possible
|
||||
/// <see cref="IBodyPart"/> that can be operated on.
|
||||
/// </summary>
|
||||
private void HandleReceiveBodyPart(int key)
|
||||
{
|
||||
if (PerformerCache == null ||
|
||||
!PerformerCache.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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) ||
|
||||
BodyCache == null)
|
||||
{
|
||||
NotUsefulAnymorePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
var target = (IBodyPart) targetObject!;
|
||||
|
||||
// TODO BODY Reconsider
|
||||
if (!target.AttemptSurgery(_surgeryType, BodyCache, this, PerformerCache))
|
||||
{
|
||||
NotUsefulAnymorePopup();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the client chooses from a list of possible
|
||||
/// <see cref="IMechanism"/> to choose from.
|
||||
/// </summary>
|
||||
private void HandleReceiveMechanism(int key)
|
||||
{
|
||||
// TODO: sanity checks to see whether user is in range, user is still able-bodied, target is still the same, etc etc
|
||||
if (BodyCache == null ||
|
||||
!_optionsCache.TryGetValue(key, out var targetObject) ||
|
||||
targetObject is not MechanismComponent target ||
|
||||
PerformerCache == null ||
|
||||
!PerformerCache.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
NotUsefulAnymorePopup();
|
||||
return;
|
||||
}
|
||||
|
||||
CloseSurgeryUI(actor.PlayerSession);
|
||||
_callbackCache?.Invoke(target, BodyCache, this, PerformerCache);
|
||||
}
|
||||
|
||||
private void NotUsefulPopup()
|
||||
{
|
||||
if (PerformerCache == null) return;
|
||||
|
||||
BodyCache?.Owner.PopupMessage(PerformerCache,
|
||||
Loc.GetString("You see no useful way to use {0:theName}.", Owner));
|
||||
}
|
||||
|
||||
private void NotUsefulAnymorePopup()
|
||||
{
|
||||
if (PerformerCache == null) return;
|
||||
|
||||
BodyCache?.Owner.PopupMessage(PerformerCache,
|
||||
Loc.GetString("You see no useful way to use {0:theName} anymore.", Owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.GameObjects.Components.Tag;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Botany
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class LogComponent : Component, IInteractUsing
|
||||
{
|
||||
public override string Name => "Log";
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(eventArgs.User))
|
||||
return false;
|
||||
|
||||
if (eventArgs.Using.HasTag("BotanySharp"))
|
||||
{
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var plank = Owner.EntityManager.SpawnEntity("MaterialWoodPlank1", Owner.Transform.Coordinates);
|
||||
plank.RandomOffset(0.25f);
|
||||
}
|
||||
|
||||
Owner.QueueDelete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,871 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.Botany;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Server.GameObjects.Components.Fluids;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Botany;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Tag;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Botany
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class PlantHolderComponent : Component, IInteractUsing, IInteractHand, IActivate, IExamine
|
||||
{
|
||||
public const float HydroponicsSpeedMultiplier = 1f;
|
||||
public const float HydroponicsConsumptionMultiplier = 4f;
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
public override string Name => "PlantHolder";
|
||||
|
||||
[ViewVariables] private int _lastProduce;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private int _missingGas;
|
||||
private readonly TimeSpan _cycleDelay = TimeSpan.FromSeconds(15f);
|
||||
[ViewVariables] private TimeSpan _lastCycle = TimeSpan.Zero;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private bool _updateSpriteAfterUpdate;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("drawWarnings")]
|
||||
public bool DrawWarnings { get; private set; } = false;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float WaterLevel { get; private set; } = 100f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float NutritionLevel { get; private set; } = 100f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float PestLevel { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float WeedLevel { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Toxins { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Age { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int SkipAging { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Dead { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Harvest { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Sampled { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int YieldMod { get; set; } = 1;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MutationMod { get; set; } = 1f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MutationLevel { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Health { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float WeedCoefficient { get; set; } = 1f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Seed? Seed { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ImproperHeat { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ImproperPressure { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ImproperLight { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ForceUpdate { get; set; }
|
||||
|
||||
[ComponentDependency] private readonly SolutionContainerComponent? _solutionContainer = default!;
|
||||
[ComponentDependency] private readonly AppearanceComponent? _appearanceComponent = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Owner.EnsureComponentWarn<SolutionContainerComponent>();
|
||||
}
|
||||
|
||||
public void WeedInvasion()
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
UpdateReagents();
|
||||
|
||||
var curTime = _gameTiming.CurTime;
|
||||
|
||||
if (ForceUpdate)
|
||||
ForceUpdate = false;
|
||||
else if (curTime < (_lastCycle + _cycleDelay))
|
||||
{
|
||||
if(_updateSpriteAfterUpdate)
|
||||
UpdateSprite();
|
||||
return;
|
||||
}
|
||||
|
||||
_lastCycle = curTime;
|
||||
|
||||
|
||||
// Weeds like water and nutrients! They may appear even if there's not a seed planted.
|
||||
if (WaterLevel > 10 && NutritionLevel > 2 && _random.Prob(Seed == null ? 0.05f : 0.01f))
|
||||
{
|
||||
WeedLevel += 1 * HydroponicsSpeedMultiplier * WeedCoefficient;
|
||||
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
// There's a chance for a weed explosion to happen if weeds take over.
|
||||
// Plants that are themselves weeds (WeedTolerance > 8) are unaffected.
|
||||
if (WeedLevel >= 10 && _random.Prob(0.1f))
|
||||
{
|
||||
if (Seed == null || WeedLevel >= Seed.WeedTolerance + 2)
|
||||
WeedInvasion();
|
||||
}
|
||||
|
||||
// If we have no seed planted, or the plant is dead, stop processing here.
|
||||
if (Seed == null || Dead)
|
||||
{
|
||||
if (_updateSpriteAfterUpdate)
|
||||
UpdateSprite();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// There's a small chance the pest population increases.
|
||||
// Can only happen when there's a live seed planted.
|
||||
if (_random.Prob(0.01f))
|
||||
{
|
||||
PestLevel += 0.5f * HydroponicsSpeedMultiplier;
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
// Advance plant age here.
|
||||
if (SkipAging > 0)
|
||||
SkipAging--;
|
||||
else
|
||||
{
|
||||
if(_random.Prob(0.8f))
|
||||
Age += (int)(1 * HydroponicsSpeedMultiplier);
|
||||
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
// Nutrient consumption.
|
||||
if (Seed.NutrientConsumption > 0 && NutritionLevel > 0 && _random.Prob(0.75f))
|
||||
{
|
||||
NutritionLevel -= MathF.Max(0f, Seed.NutrientConsumption * HydroponicsSpeedMultiplier);
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
// Water consumption.
|
||||
if (Seed.WaterConsumption > 0 && WaterLevel > 0 && _random.Prob(0.75f))
|
||||
{
|
||||
WaterLevel -= MathF.Max(0f, Seed.NutrientConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier);
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
var healthMod = _random.Next(1, 3) * HydroponicsSpeedMultiplier;
|
||||
|
||||
// Make sure the plant is not starving.
|
||||
if (_random.Prob(0.35f))
|
||||
{
|
||||
if (NutritionLevel > 2)
|
||||
{
|
||||
Health += healthMod;
|
||||
}
|
||||
else
|
||||
{
|
||||
AffectGrowth(-1);
|
||||
Health -= healthMod;
|
||||
}
|
||||
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
// Make sure the plant is not thirsty.
|
||||
if (_random.Prob(0.35f))
|
||||
{
|
||||
if (WaterLevel > 10)
|
||||
{
|
||||
Health += healthMod;
|
||||
}
|
||||
else
|
||||
{
|
||||
AffectGrowth(-1);
|
||||
Health -= healthMod;
|
||||
}
|
||||
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
|
||||
var environment = tileAtmos?.Air ?? GasMixture.SpaceGas;
|
||||
|
||||
if (Seed.ConsumeGasses.Count > 0)
|
||||
{
|
||||
_missingGas = 0;
|
||||
|
||||
foreach (var (gas, amount) in Seed.ConsumeGasses)
|
||||
{
|
||||
if (environment.GetMoles(gas) < amount)
|
||||
{
|
||||
_missingGas++;
|
||||
continue;
|
||||
}
|
||||
|
||||
environment.AdjustMoles(gas, -amount);
|
||||
}
|
||||
|
||||
if (_missingGas > 0)
|
||||
{
|
||||
Health -= _missingGas * HydroponicsSpeedMultiplier;
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Seed pressure resistance.
|
||||
var pressure = environment.Pressure;
|
||||
if (pressure < Seed.LowPressureTolerance || pressure > Seed.HighPressureTolerance)
|
||||
{
|
||||
Health -= healthMod;
|
||||
ImproperPressure = true;
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ImproperPressure = false;
|
||||
}
|
||||
|
||||
// Seed ideal temperature.
|
||||
if (MathF.Abs(environment.Temperature - Seed.IdealHeat) > Seed.HeatTolerance)
|
||||
{
|
||||
Health -= healthMod;
|
||||
ImproperHeat = true;
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ImproperHeat = false;
|
||||
}
|
||||
|
||||
// Gas production.
|
||||
var exudeCount = Seed.ExudeGasses.Count;
|
||||
if (exudeCount > 0)
|
||||
{
|
||||
foreach (var (gas, amount) in Seed.ExudeGasses)
|
||||
{
|
||||
environment.AdjustMoles(gas, MathF.Max(1f, MathF.Round((amount * MathF.Round(Seed.Potency)) / exudeCount)));
|
||||
}
|
||||
}
|
||||
|
||||
// Toxin levels beyond the plant's tolerance cause damage.
|
||||
// They are, however, slowly reduced over time.
|
||||
if (Toxins > 0)
|
||||
{
|
||||
var toxinUptake = MathF.Max(1, MathF.Round(Toxins / 10f));
|
||||
if (Toxins > Seed.ToxinsTolerance)
|
||||
{
|
||||
Health -= toxinUptake;
|
||||
}
|
||||
|
||||
Toxins -= toxinUptake;
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
// Weed levels.
|
||||
if (PestLevel > 0)
|
||||
{
|
||||
// TODO: Carnivorous plants?
|
||||
if (PestLevel > Seed.PestTolerance)
|
||||
{
|
||||
Health -= HydroponicsSpeedMultiplier;
|
||||
}
|
||||
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
// Weed levels.
|
||||
if (WeedLevel > 0)
|
||||
{
|
||||
// TODO: Parasitic plants.
|
||||
if (WeedLevel >= Seed.WeedTolerance)
|
||||
{
|
||||
Health -= HydroponicsSpeedMultiplier;
|
||||
}
|
||||
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
|
||||
if (Age > Seed.Lifespan)
|
||||
{
|
||||
Health -= _random.Next(3, 5) * HydroponicsSpeedMultiplier;
|
||||
if (DrawWarnings)
|
||||
_updateSpriteAfterUpdate = true;
|
||||
}
|
||||
else if(Age < 0) // Revert back to seed packet!
|
||||
{
|
||||
Seed.SpawnSeedPacket(Owner.Transform.Coordinates);
|
||||
RemovePlant();
|
||||
ForceUpdate = true;
|
||||
Update();
|
||||
}
|
||||
|
||||
CheckHealth();
|
||||
|
||||
if (Harvest && Seed.HarvestRepeat == HarvestType.SelfHarvest)
|
||||
AutoHarvest();
|
||||
|
||||
// If enough time has passed since the plant was harvested, we're ready to harvest again!
|
||||
if (!Dead && Seed.ProductPrototypes.Count > 0)
|
||||
{
|
||||
if (Age > Seed.Production)
|
||||
{
|
||||
if ((Age - _lastProduce) > Seed.Production && !Harvest)
|
||||
{
|
||||
Harvest = true;
|
||||
_lastProduce = Age;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Harvest)
|
||||
{
|
||||
Harvest = false;
|
||||
_lastProduce = Age;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckLevelSanity();
|
||||
|
||||
if(_updateSpriteAfterUpdate)
|
||||
UpdateSprite();
|
||||
}
|
||||
|
||||
private void CheckLevelSanity()
|
||||
{
|
||||
if (Seed != null)
|
||||
Health = MathHelper.Clamp(Health, 0, Seed.Endurance);
|
||||
else
|
||||
{
|
||||
Health = 0f;
|
||||
Dead = false;
|
||||
}
|
||||
|
||||
MutationLevel = MathHelper.Clamp(MutationLevel, 0f, 100f);
|
||||
NutritionLevel = MathHelper.Clamp(NutritionLevel, 0f, 100f);
|
||||
WaterLevel = MathHelper.Clamp(WaterLevel, 0f, 100f);
|
||||
PestLevel = MathHelper.Clamp(PestLevel, 0f, 10f);
|
||||
WeedLevel = MathHelper.Clamp(WeedLevel, 0f, 10f);
|
||||
Toxins = MathHelper.Clamp(Toxins, 0f, 100f);
|
||||
YieldMod = MathHelper.Clamp(YieldMod, 0, 2);
|
||||
MutationMod = MathHelper.Clamp(MutationMod, 0f, 3f);
|
||||
}
|
||||
|
||||
public bool DoHarvest(IEntity user)
|
||||
{
|
||||
if (Seed == null || user.Deleted || !ActionBlockerSystem.CanInteract(user))
|
||||
return false;
|
||||
|
||||
if (Harvest && !Dead)
|
||||
{
|
||||
if (user.TryGetComponent(out HandsComponent? hands))
|
||||
{
|
||||
if (!Seed.CheckHarvest(user, hands.GetActiveHand?.Owner))
|
||||
return false;
|
||||
|
||||
} else if (!Seed.CheckHarvest(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Seed.Harvest(user, YieldMod);
|
||||
AfterHarvest();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Dead) return false;
|
||||
|
||||
RemovePlant();
|
||||
AfterHarvest();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AutoHarvest()
|
||||
{
|
||||
if (Seed == null || !Harvest)
|
||||
return;
|
||||
|
||||
Seed.AutoHarvest(Owner.Transform.Coordinates);
|
||||
AfterHarvest();
|
||||
}
|
||||
|
||||
private void AfterHarvest()
|
||||
{
|
||||
Harvest = false;
|
||||
_lastProduce = Age;
|
||||
|
||||
if(Seed?.HarvestRepeat == HarvestType.NoRepeat)
|
||||
RemovePlant();
|
||||
|
||||
CheckLevelSanity();
|
||||
UpdateSprite();
|
||||
}
|
||||
|
||||
public void CheckHealth()
|
||||
{
|
||||
if (Health <= 0)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
public void Die()
|
||||
{
|
||||
Dead = true;
|
||||
Harvest = false;
|
||||
MutationLevel = 0;
|
||||
YieldMod = 1;
|
||||
MutationMod = 1;
|
||||
ImproperLight = false;
|
||||
ImproperHeat = false;
|
||||
ImproperPressure = false;
|
||||
WeedLevel += 1 * HydroponicsSpeedMultiplier;
|
||||
PestLevel = 0;
|
||||
UpdateSprite();
|
||||
}
|
||||
|
||||
public void RemovePlant()
|
||||
{
|
||||
YieldMod = 1;
|
||||
MutationMod = 1;
|
||||
PestLevel = 0;
|
||||
Seed = null;
|
||||
Dead = false;
|
||||
Age = 0;
|
||||
Sampled = false;
|
||||
Harvest = false;
|
||||
ImproperLight = false;
|
||||
ImproperPressure = false;
|
||||
ImproperHeat = false;
|
||||
|
||||
UpdateSprite();
|
||||
}
|
||||
|
||||
public void AffectGrowth(int amount)
|
||||
{
|
||||
if (Seed == null)
|
||||
return;
|
||||
|
||||
if (amount > 0)
|
||||
{
|
||||
if (Age < Seed.Maturation)
|
||||
Age += amount;
|
||||
else if (!Harvest && Seed.Yield <= 0f)
|
||||
_lastProduce -= amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Age < Seed.Maturation)
|
||||
SkipAging++;
|
||||
else if (!Harvest && Seed.Yield <= 0f)
|
||||
_lastProduce += amount;
|
||||
}
|
||||
}
|
||||
|
||||
public void AdjustNutrient(float amount)
|
||||
{
|
||||
NutritionLevel += amount;
|
||||
}
|
||||
|
||||
public void AdjustWater(float amount)
|
||||
{
|
||||
WaterLevel += amount;
|
||||
|
||||
// Water dilutes toxins.
|
||||
if (amount > 0)
|
||||
{
|
||||
Toxins -= amount * 4f;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateReagents()
|
||||
{
|
||||
if (_solutionContainer == null)
|
||||
return;
|
||||
|
||||
if (_solutionContainer.Solution.TotalVolume <= 0 || MutationLevel >= 25)
|
||||
{
|
||||
if (MutationLevel >= 0)
|
||||
{
|
||||
Mutate(Math.Min(MutationLevel, 25));
|
||||
MutationLevel = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var one = ReagentUnit.New(1);
|
||||
|
||||
foreach (var (reagent, amount) in _solutionContainer.ReagentList.ToArray())
|
||||
{
|
||||
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent);
|
||||
reagentProto.ReactionPlant(Owner);
|
||||
_solutionContainer.Solution.RemoveReagent(reagent, amount < one ? amount : one);
|
||||
}
|
||||
}
|
||||
|
||||
CheckLevelSanity();
|
||||
}
|
||||
|
||||
private void Mutate(float severity)
|
||||
{
|
||||
// TODO: Coming soon in "Botany 2: Plant boogaloo".
|
||||
}
|
||||
|
||||
public void UpdateSprite()
|
||||
{
|
||||
_updateSpriteAfterUpdate = false;
|
||||
|
||||
if (_appearanceComponent == null)
|
||||
return;
|
||||
|
||||
if (Seed != null)
|
||||
{
|
||||
if(DrawWarnings)
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.HealthLight, Health <= (Seed.Endurance / 2f));
|
||||
|
||||
if (Dead)
|
||||
{
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi, "dead"));
|
||||
}
|
||||
else if (Harvest)
|
||||
{
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi, "harvest"));
|
||||
}
|
||||
else if (Age < Seed.Maturation)
|
||||
{
|
||||
var growthStage = Math.Max(1, (int)((Age * Seed.GrowthStages) / Seed.Maturation));
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi,$"stage-{growthStage}"));
|
||||
_lastProduce = Age;
|
||||
}
|
||||
else
|
||||
{
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi,$"stage-{Seed.GrowthStages}"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.Plant, SpriteSpecifier.Invalid);
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.HealthLight, false);
|
||||
}
|
||||
|
||||
if (!DrawWarnings) return;
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.WaterLight, WaterLevel <= 10);
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.NutritionLight, NutritionLevel <= 2);
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.AlertLight, WeedLevel >= 5 || PestLevel >= 5 || Toxins >= 40 || ImproperHeat || ImproperLight || ImproperPressure || _missingGas > 0);
|
||||
_appearanceComponent.SetData(PlantHolderVisuals.HarvestLight, Harvest);
|
||||
}
|
||||
|
||||
public void CheckForDivergence(bool modified)
|
||||
{
|
||||
// Make sure we're not modifying a "global" seed.
|
||||
// If this seed is not in the global seed list, then no products of this line have been harvested yet.
|
||||
// It is then safe to assume it's restricted to this tray.
|
||||
if (Seed == null) return;
|
||||
var plantSystem = EntitySystem.Get<PlantSystem>();
|
||||
if (plantSystem.Seeds.ContainsKey(Seed.Uid))
|
||||
Seed = Seed.Diverge(modified);
|
||||
}
|
||||
|
||||
private void ForceUpdateByExternalCause()
|
||||
{
|
||||
SkipAging++; // We're forcing an update cycle, so one age hasn't passed.
|
||||
ForceUpdate = true;
|
||||
Update();
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
var user = eventArgs.User;
|
||||
var usingItem = eventArgs.Using;
|
||||
|
||||
if (usingItem == null || usingItem.Deleted || !ActionBlockerSystem.CanInteract(user))
|
||||
return false;
|
||||
|
||||
if (usingItem.TryGetComponent(out SeedComponent? seeds))
|
||||
{
|
||||
if (Seed == null)
|
||||
{
|
||||
if (seeds.Seed == null)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("The packet seems to be empty. You throw it away."));
|
||||
usingItem.QueueDelete();
|
||||
return false;
|
||||
}
|
||||
|
||||
user.PopupMessageCursor(Loc.GetString("You plant the {0} {1}.", seeds.Seed.SeedName, seeds.Seed.SeedNoun));
|
||||
|
||||
Seed = seeds.Seed;
|
||||
Dead = false;
|
||||
Age = 1;
|
||||
Health = Seed.Endurance;
|
||||
_lastCycle = _gameTiming.CurTime;
|
||||
|
||||
usingItem.QueueDelete();
|
||||
|
||||
CheckLevelSanity();
|
||||
UpdateSprite();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
user.PopupMessageCursor(Loc.GetString("The {0} already has seeds in it!", Owner.Name));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (usingItem.HasTag("Hoe"))
|
||||
{
|
||||
if (WeedLevel > 0)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("You remove the weeds from the {0}.", Owner.Name));
|
||||
user.PopupMessageOtherClients(Loc.GetString("{0} starts uprooting the weeds.", user.Name));
|
||||
WeedLevel = 0;
|
||||
UpdateSprite();
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("This plot is devoid of weeds! It doesn't need uprooting."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (usingItem.HasTag("Shovel"))
|
||||
{
|
||||
if (Seed != null)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("You remove the plant from the {0}.", Owner.Name));
|
||||
user.PopupMessageOtherClients(Loc.GetString("{0} removes the plant.", user.Name));
|
||||
RemovePlant();
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("There is no plant to remove."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (usingItem.TryGetComponent(out ISolutionInteractionsComponent? solution) && solution.CanDrain)
|
||||
{
|
||||
var amount = ReagentUnit.New(5);
|
||||
var sprayed = false;
|
||||
|
||||
if (usingItem.TryGetComponent(out SprayComponent? spray))
|
||||
{
|
||||
sprayed = true;
|
||||
amount = ReagentUnit.New(1);
|
||||
|
||||
if (!string.IsNullOrEmpty(spray.SpraySound))
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(usingItem), spray.SpraySound, usingItem, AudioHelpers.WithVariation(0.125f));
|
||||
}
|
||||
}
|
||||
|
||||
var split = solution.Drain(amount);
|
||||
if (split.TotalVolume == 0)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("{0:TheName} is empty!", usingItem));
|
||||
return true;
|
||||
}
|
||||
|
||||
user.PopupMessageCursor(Loc.GetString(
|
||||
sprayed ? "You spray {0:TheName}" : "You transfer {1}u to {0:TheName}",
|
||||
Owner, split.TotalVolume));
|
||||
|
||||
_solutionContainer?.TryAddSolution(split);
|
||||
|
||||
ForceUpdateByExternalCause();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (usingItem.HasTag("PlantSampleTaker"))
|
||||
{
|
||||
if (Seed == null)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("There is nothing to take a sample of!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Sampled)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("This plant has already been sampled."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Dead)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("This plant is dead."));
|
||||
return false;
|
||||
}
|
||||
|
||||
var seed = Seed.SpawnSeedPacket(user.Transform.Coordinates);
|
||||
seed.RandomOffset(0.25f);
|
||||
user.PopupMessageCursor(Loc.GetString($"You take a sample from the {Seed.DisplayName}."));
|
||||
Health -= (_random.Next(3, 5) * 10);
|
||||
|
||||
if (_random.Prob(0.3f))
|
||||
Sampled = true;
|
||||
|
||||
// Just in case.
|
||||
CheckLevelSanity();
|
||||
ForceUpdateByExternalCause();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (usingItem.HasTag("BotanySharp"))
|
||||
{
|
||||
return DoHarvest(user);
|
||||
}
|
||||
|
||||
if (usingItem.HasComponent<ProduceComponent>())
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("You compost {1:theName} into {0:theName}.", Owner, usingItem));
|
||||
user.PopupMessageOtherClients(Loc.GetString("{0:TheName} composts {1:theName} into {2:theName}.", user, usingItem, Owner));
|
||||
|
||||
if (usingItem.TryGetComponent(out SolutionContainerComponent? solution2))
|
||||
{
|
||||
// This deliberately discards overfill.
|
||||
_solutionContainer?.TryAddSolution(solution2.SplitSolution(solution2.Solution.TotalVolume));
|
||||
|
||||
ForceUpdateByExternalCause();
|
||||
}
|
||||
|
||||
usingItem.QueueDelete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs)
|
||||
{
|
||||
// DoHarvest does the sanity checks.
|
||||
return DoHarvest(eventArgs.User);
|
||||
}
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
// DoHarvest does the sanity checks.
|
||||
DoHarvest(eventArgs.User);
|
||||
}
|
||||
|
||||
public void Examine(FormattedMessage message, bool inDetailsRange)
|
||||
{
|
||||
if (!inDetailsRange)
|
||||
return;
|
||||
|
||||
if (Seed == null)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("It has nothing planted in it.\n"));
|
||||
}
|
||||
else if (!Dead)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString($"[color=green]{Seed.DisplayName}[/color] {(Seed.DisplayName.EndsWith('s') ? "are" : "is")} growing here.\n"));
|
||||
|
||||
if(Health <= Seed.Endurance / 2)
|
||||
message.AddMarkup(Loc.GetString($"The plant looks [color=red]{(Age > Seed.Lifespan ? "old and wilting" : "unhealthy")}[/color].\n"));
|
||||
}
|
||||
else
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("It is full of [color=red]dead plant matter[/color].\n"));
|
||||
}
|
||||
|
||||
if(WeedLevel >= 5)
|
||||
message.AddMarkup(Loc.GetString("It is filled with [color=green]weeds[/color]!\n"));
|
||||
|
||||
if(PestLevel >= 5)
|
||||
message.AddMarkup(Loc.GetString("It is filled with [color=gray]tiny worms[/color]!\n"));
|
||||
|
||||
message.AddMarkup(Loc.GetString($"Water: [color=cyan]{(int)WaterLevel}[/color]\n"));
|
||||
message.AddMarkup(Loc.GetString($"Nutrient: [color=orange]{(int)NutritionLevel}[/color]\n"));
|
||||
|
||||
if (DrawWarnings)
|
||||
{
|
||||
if(Toxins > 40f)
|
||||
message.AddMarkup(Loc.GetString("The [color=red]toxicity level alert[/color] is flashing red.\n"));
|
||||
|
||||
if(ImproperLight)
|
||||
message.AddMarkup(Loc.GetString("The [color=yellow]improper light level alert[/color] is blinking.\n"));
|
||||
|
||||
if(ImproperHeat)
|
||||
message.AddMarkup(Loc.GetString("The [color=orange]improper temperature level alert[/color] is blinking.\n"));
|
||||
|
||||
if(ImproperPressure)
|
||||
message.AddMarkup(Loc.GetString("The [color=lightblue]improper environment pressure alert[/color] is blinking.\n"));
|
||||
|
||||
if(_missingGas > 0)
|
||||
message.AddMarkup(Loc.GetString("The [color=cyan]improper gas environment alert[/color] is blinking.\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.Botany;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.Chemistry;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Botany
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ProduceComponent : Component, ISerializationHooks
|
||||
{
|
||||
public override string Name => "Produce";
|
||||
|
||||
[DataField("seed")]
|
||||
private string? _seedName;
|
||||
|
||||
[ViewVariables]
|
||||
public Seed? Seed
|
||||
{
|
||||
get => _seedName != null ? IoCManager.Resolve<IPrototypeManager>().Index<Seed>(_seedName) : null;
|
||||
set => _seedName = value?.ID;
|
||||
}
|
||||
|
||||
public float Potency => Seed?.Potency ?? 0;
|
||||
|
||||
public void Grown()
|
||||
{
|
||||
if (Seed == null)
|
||||
return;
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.LayerSetRSI(0, Seed.PlantRsi);
|
||||
sprite.LayerSetState(0, Seed.PlantIconState);
|
||||
}
|
||||
|
||||
var solutionContainer = Owner.EnsureComponent<SolutionContainerComponent>();
|
||||
|
||||
solutionContainer.RemoveAllSolution();
|
||||
|
||||
foreach (var (chem, quantity) in Seed.Chemicals)
|
||||
{
|
||||
var amount = ReagentUnit.New(quantity.Min);
|
||||
if(quantity.PotencyDivisor > 0 && Potency > 0)
|
||||
amount += ReagentUnit.New(Potency/quantity.PotencyDivisor);
|
||||
amount = ReagentUnit.New((int) MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max));
|
||||
solutionContainer.MaxVolume += amount;
|
||||
solutionContainer.Solution.AddReagent(chem, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.Botany;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Botany
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class SeedComponent : Component, IExamine
|
||||
{
|
||||
public override string Name => "Seed";
|
||||
|
||||
[DataField("seed")]
|
||||
private string? _seedName;
|
||||
|
||||
[ViewVariables]
|
||||
public Seed? Seed
|
||||
{
|
||||
get => _seedName != null ? IoCManager.Resolve<IPrototypeManager>().Index<Seed>(_seedName) : null;
|
||||
set => _seedName = value?.ID;
|
||||
}
|
||||
|
||||
public void Examine(FormattedMessage message, bool inDetailsRange)
|
||||
{
|
||||
if (!inDetailsRange)
|
||||
return;
|
||||
|
||||
if (Seed == null)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("It doesn't seem to contain any seeds.\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
message.AddMarkup(Loc.GetString($"It has a picture of [color=yellow]{Seed.DisplayName}[/color] on the front.\n"));
|
||||
|
||||
if(!Seed.RoundStart)
|
||||
message.AddMarkup(Loc.GetString($"It's tagged as variety [color=lightgray]no. {Seed.Uid}[/color].\n"));
|
||||
else
|
||||
{
|
||||
message.AddMarkup(Loc.GetString($"Plant Yield: [color=lightblue]{Seed.Yield}[/color]\n"));
|
||||
message.AddMarkup(Loc.GetString($"Plant Potency: [color=lightblue]{Seed.Potency}[/color]\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Botany
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class SeedExtractorComponent : Component, IInteractUsing
|
||||
{
|
||||
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiver = default!;
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override string Name => "SeedExtractor";
|
||||
|
||||
// TODO: Upgradeable machines.
|
||||
private int _minSeeds = 1;
|
||||
private int _maxSeeds = 4;
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!_powerReceiver?.Powered ?? false)
|
||||
return false;
|
||||
|
||||
if (eventArgs.Using.TryGetComponent(out ProduceComponent? produce) && produce.Seed != null)
|
||||
{
|
||||
eventArgs.User.PopupMessageCursor(Loc.GetString("You extract some seeds from the {0}.", eventArgs.Using.Name));
|
||||
|
||||
eventArgs.Using.QueueDelete();
|
||||
|
||||
var random = _random.Next(_minSeeds, _maxSeeds);
|
||||
|
||||
for (var i = 0; i < random; i++)
|
||||
{
|
||||
produce.Seed.SpawnSeedPacket(Owner.Transform.Coordinates, Owner.EntityManager);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,457 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Mobs.State;
|
||||
using Content.Server.GameObjects.Components.Pulling;
|
||||
using Content.Server.GameObjects.Components.Strap;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.GameObjects.Components.Buckle;
|
||||
using Content.Shared.GameObjects.Components.Strap;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Buckle
|
||||
{
|
||||
/// <summary>
|
||||
/// Component that handles sitting entities into <see cref="StrapComponent"/>s.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedBuckleComponent))]
|
||||
public class BuckleComponent : SharedBuckleComponent
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
[ComponentDependency] public readonly AppearanceComponent? Appearance = null;
|
||||
[ComponentDependency] private readonly ServerAlertsComponent? _serverAlerts = null;
|
||||
[ComponentDependency] private readonly StunnableComponent? _stunnable = null;
|
||||
[ComponentDependency] private readonly MobStateComponent? _mobState = null;
|
||||
|
||||
[DataField("size")]
|
||||
private int _size = 100;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time that must pass for this entity to
|
||||
/// be able to unbuckle after recently buckling.
|
||||
/// </summary>
|
||||
[DataField("delay")]
|
||||
[ViewVariables]
|
||||
private TimeSpan _unbuckleDelay = TimeSpan.FromSeconds(0.25f);
|
||||
|
||||
/// <summary>
|
||||
/// The time that this entity buckled at.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private TimeSpan _buckleTime;
|
||||
|
||||
/// <summary>
|
||||
/// The position offset that is being applied to this entity if buckled.
|
||||
/// </summary>
|
||||
public Vector2 BuckleOffset { get; private set; }
|
||||
|
||||
private StrapComponent? _buckledTo;
|
||||
|
||||
/// <summary>
|
||||
/// The strap that this component is buckled to.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public StrapComponent? BuckledTo
|
||||
{
|
||||
get => _buckledTo;
|
||||
private set
|
||||
{
|
||||
_buckledTo = value;
|
||||
_buckleTime = _gameTiming.CurTime;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
public override bool Buckled => BuckledTo != null;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of space that this entity occupies in a
|
||||
/// <see cref="StrapComponent"/>.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public int Size => _size;
|
||||
|
||||
/// <summary>
|
||||
/// Shows or hides the buckled status effect depending on if the
|
||||
/// entity is buckled or not.
|
||||
/// </summary>
|
||||
private void UpdateBuckleStatus()
|
||||
{
|
||||
if (_serverAlerts == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Buckled)
|
||||
{
|
||||
_serverAlerts.ShowAlert(BuckledTo?.BuckledAlertType ?? AlertType.Buckled);
|
||||
}
|
||||
else
|
||||
{
|
||||
_serverAlerts.ClearAlertCategory(AlertCategory.Buckled);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reattaches this entity to the strap, modifying its position and rotation.
|
||||
/// </summary>
|
||||
/// <param name="strap">The strap to reattach to.</param>
|
||||
public void ReAttach(StrapComponent strap)
|
||||
{
|
||||
var ownTransform = Owner.Transform;
|
||||
var strapTransform = strap.Owner.Transform;
|
||||
|
||||
ownTransform.AttachParent(strapTransform);
|
||||
|
||||
switch (strap.Position)
|
||||
{
|
||||
case StrapPosition.None:
|
||||
ownTransform.WorldRotation = strapTransform.WorldRotation;
|
||||
break;
|
||||
case StrapPosition.Stand:
|
||||
EntitySystem.Get<StandingStateSystem>().Standing(Owner);
|
||||
ownTransform.WorldRotation = strapTransform.WorldRotation;
|
||||
break;
|
||||
case StrapPosition.Down:
|
||||
EntitySystem.Get<StandingStateSystem>().Down(Owner, force: true);
|
||||
ownTransform.WorldRotation = Angle.South;
|
||||
break;
|
||||
}
|
||||
|
||||
// Assign BuckleOffset first, before causing a MoveEvent to fire
|
||||
if (strapTransform.WorldRotation.GetCardinalDir() == Direction.North)
|
||||
{
|
||||
BuckleOffset = (0, 0.15f);
|
||||
ownTransform.WorldPosition = strapTransform.WorldPosition + BuckleOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
BuckleOffset = Vector2.Zero;
|
||||
ownTransform.WorldPosition = strapTransform.WorldPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanBuckle(IEntity? user, IEntity to, [NotNullWhen(true)] out StrapComponent? strap)
|
||||
{
|
||||
strap = null;
|
||||
|
||||
if (user == null || user == to)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You can't do that!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!to.TryGetComponent(out strap))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var component = strap;
|
||||
bool Ignored(IEntity entity) => entity == Owner || entity == user || entity == component.Owner;
|
||||
|
||||
if (!Owner.InRangeUnobstructed(strap, Range, predicate: Ignored, popup: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If in a container
|
||||
if (Owner.TryGetContainer(out var ownerContainer))
|
||||
{
|
||||
// And not in the same container as the strap
|
||||
if (!strap.Owner.TryGetContainer(out var strapContainer) ||
|
||||
ownerContainer != strapContainer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.HasComponent<HandsComponent>())
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You don't have hands!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Buckled)
|
||||
{
|
||||
var message = Loc.GetString(Owner == user
|
||||
? "You are already buckled in!"
|
||||
: "{0:They} are already buckled in!", Owner);
|
||||
Owner.PopupMessage(user, message);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var parent = to.Transform.Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
if (parent == user.Transform)
|
||||
{
|
||||
var message = Loc.GetString(Owner == user
|
||||
? "You can't buckle yourself there!"
|
||||
: "You can't buckle {0:them} there!", Owner);
|
||||
Owner.PopupMessage(user, message);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
if (!strap.HasSpace(this))
|
||||
{
|
||||
var message = Loc.GetString(Owner == user
|
||||
? "You can't fit there!"
|
||||
: "{0:They} can't fit there!", Owner);
|
||||
Owner.PopupMessage(user, message);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool TryBuckle(IEntity? user, IEntity to)
|
||||
{
|
||||
if (user == null || !CanBuckle(user, to, out var strap))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(Owner), strap.BuckleSound, Owner);
|
||||
|
||||
if (!strap.TryAdd(this))
|
||||
{
|
||||
var message = Loc.GetString(Owner == user
|
||||
? "You can't buckle yourself there!"
|
||||
: "You can't buckle {0:them} there!", Owner);
|
||||
Owner.PopupMessage(user, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
Appearance?.SetData(BuckleVisuals.Buckled, true);
|
||||
|
||||
ReAttach(strap);
|
||||
|
||||
BuckledTo = strap;
|
||||
LastEntityBuckledTo = BuckledTo.Owner.Uid;
|
||||
DontCollide = true;
|
||||
|
||||
UpdateBuckleStatus();
|
||||
|
||||
SendMessage(new BuckleMessage(Owner, to));
|
||||
|
||||
if (Owner.TryGetComponent(out PullableComponent? ownerPullable))
|
||||
{
|
||||
if (ownerPullable.Puller != null)
|
||||
{
|
||||
ownerPullable.TryStopPull();
|
||||
}
|
||||
}
|
||||
|
||||
if (to.TryGetComponent(out PullableComponent? toPullable))
|
||||
{
|
||||
if (toPullable.Puller == Owner)
|
||||
{
|
||||
// can't pull it and buckle to it at the same time
|
||||
toPullable.TryStopPull();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to unbuckle the Owner of this component from its current strap.
|
||||
/// </summary>
|
||||
/// <param name="user">The entity doing the unbuckling.</param>
|
||||
/// <param name="force">
|
||||
/// Whether to force the unbuckling or not. Does not guarantee true to
|
||||
/// be returned, but guarantees the owner to be unbuckled afterwards.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// true if the owner was unbuckled, otherwise false even if the owner
|
||||
/// was previously already unbuckled.
|
||||
/// </returns>
|
||||
public bool TryUnbuckle(IEntity user, bool force = false)
|
||||
{
|
||||
if (BuckledTo == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldBuckledTo = BuckledTo;
|
||||
|
||||
if (!force)
|
||||
{
|
||||
if (_gameTiming.CurTime < _buckleTime + _unbuckleDelay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You can't do that!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.InRangeUnobstructed(oldBuckledTo, Range, popup: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BuckledTo = null;
|
||||
|
||||
if (Owner.Transform.Parent == oldBuckledTo.Owner.Transform)
|
||||
{
|
||||
Owner.Transform.AttachParentToContainerOrGrid();
|
||||
Owner.Transform.WorldRotation = oldBuckledTo.Owner.Transform.WorldRotation;
|
||||
}
|
||||
|
||||
Appearance?.SetData(BuckleVisuals.Buckled, false);
|
||||
|
||||
if (_stunnable != null && _stunnable.KnockedDown
|
||||
|| (_mobState?.IsIncapacitated() ?? false))
|
||||
{
|
||||
EntitySystem.Get<StandingStateSystem>().Down(Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntitySystem.Get<StandingStateSystem>().Standing(Owner);
|
||||
}
|
||||
|
||||
_mobState?.CurrentState?.EnterState(Owner);
|
||||
|
||||
UpdateBuckleStatus();
|
||||
|
||||
oldBuckledTo.Remove(this);
|
||||
SoundSystem.Play(Filter.Pvs(Owner), oldBuckledTo.UnbuckleSound, Owner);
|
||||
|
||||
SendMessage(new UnbuckleMessage(Owner, oldBuckledTo.Owner));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes an entity toggle the buckling status of the owner to a
|
||||
/// specific entity.
|
||||
/// </summary>
|
||||
/// <param name="user">The entity doing the buckling/unbuckling.</param>
|
||||
/// <param name="to">
|
||||
/// The entity to toggle the buckle status of the owner to.
|
||||
/// </param>
|
||||
/// <param name="force">
|
||||
/// Whether to force the unbuckling or not, if it happens. Does not
|
||||
/// guarantee true to be returned, but guarantees the owner to be
|
||||
/// unbuckled afterwards.
|
||||
/// </param>
|
||||
/// <returns>true if the buckling status was changed, false otherwise.</returns>
|
||||
public bool ToggleBuckle(IEntity user, IEntity to, bool force = false)
|
||||
{
|
||||
if (BuckledTo?.Owner == to)
|
||||
{
|
||||
return TryUnbuckle(user, force);
|
||||
}
|
||||
|
||||
return TryBuckle(user, to);
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
UpdateBuckleStatus();
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
BuckledTo?.Remove(this);
|
||||
TryUnbuckle(Owner, true);
|
||||
|
||||
_buckleTime = default;
|
||||
UpdateBuckleStatus();
|
||||
|
||||
base.OnRemove();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
int? drawDepth = null;
|
||||
|
||||
if (BuckledTo != null &&
|
||||
Owner.Transform.WorldRotation.GetCardinalDir() == Direction.North &&
|
||||
BuckledTo.SpriteComponent != null)
|
||||
{
|
||||
drawDepth = BuckledTo.SpriteComponent.DrawDepth - 1;
|
||||
}
|
||||
|
||||
return new BuckleComponentState(Buckled, drawDepth, LastEntityBuckledTo, DontCollide);
|
||||
}
|
||||
|
||||
public void Update(PhysicsComponent physics)
|
||||
{
|
||||
if (!DontCollide)
|
||||
return;
|
||||
|
||||
physics.WakeBody();
|
||||
|
||||
if (!IsOnStrapEntityThisFrame && DontCollide)
|
||||
{
|
||||
DontCollide = false;
|
||||
TryUnbuckle(Owner);
|
||||
Dirty();
|
||||
}
|
||||
|
||||
IsOnStrapEntityThisFrame = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the unbuckling of the owning entity through a verb if
|
||||
/// anyone right clicks them.
|
||||
/// </summary>
|
||||
[Verb]
|
||||
private sealed class BuckleVerb : Verb<BuckleComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, BuckleComponent component, VerbData data)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(user) || !component.Buckled)
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Unbuckle");
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, BuckleComponent component)
|
||||
{
|
||||
component.TryUnbuckle(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Cargo;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Cargo;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Prototypes.Cargo;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Cargo
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
public class CargoConsoleComponent : SharedCargoConsoleComponent, IActivate
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
[ViewVariables]
|
||||
public int Points = 1000;
|
||||
|
||||
private CargoBankAccount? _bankAccount;
|
||||
|
||||
[ViewVariables]
|
||||
public CargoBankAccount? BankAccount
|
||||
{
|
||||
get => _bankAccount;
|
||||
private set
|
||||
{
|
||||
if (_bankAccount == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_bankAccount != null)
|
||||
{
|
||||
_bankAccount.OnBalanceChange -= UpdateUIState;
|
||||
}
|
||||
|
||||
_bankAccount = value;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
value.OnBalanceChange += UpdateUIState;
|
||||
}
|
||||
|
||||
UpdateUIState();
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("requestOnly")]
|
||||
private bool _requestOnly = false;
|
||||
|
||||
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
|
||||
private CargoConsoleSystem _cargoConsoleSystem = default!;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CargoConsoleUiKey.Key);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Owner.EnsureComponentWarn(out GalacticMarketComponent _);
|
||||
Owner.EnsureComponentWarn(out CargoOrderDatabaseComponent _);
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
}
|
||||
|
||||
_cargoConsoleSystem = EntitySystem.Get<CargoConsoleSystem>();
|
||||
BankAccount = _cargoConsoleSystem.StationAccount;
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage -= UserInterfaceOnOnReceiveMessage;
|
||||
}
|
||||
|
||||
base.OnRemove();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out CargoOrderDatabaseComponent? orders))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = serverMsg.Message;
|
||||
if (orders.Database == null)
|
||||
return;
|
||||
if (!Powered)
|
||||
return;
|
||||
switch (message)
|
||||
{
|
||||
case CargoConsoleAddOrderMessage msg:
|
||||
{
|
||||
if (msg.Amount <= 0 || _bankAccount == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_cargoConsoleSystem.AddOrder(orders.Database.Id, msg.Requester, msg.Reason, msg.ProductId,
|
||||
msg.Amount, _bankAccount.Id))
|
||||
{
|
||||
SoundSystem.Play(Filter.Local(), "/Audio/Effects/error.ogg", Owner, AudioParams.Default);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CargoConsoleRemoveOrderMessage msg:
|
||||
{
|
||||
_cargoConsoleSystem.RemoveOrder(orders.Database.Id, msg.OrderNumber);
|
||||
break;
|
||||
}
|
||||
case CargoConsoleApproveOrderMessage msg:
|
||||
{
|
||||
if (_requestOnly ||
|
||||
!orders.Database.TryGetOrder(msg.OrderNumber, out var order) ||
|
||||
_bankAccount == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
PrototypeManager.TryIndex(order.ProductId, out CargoProductPrototype? product);
|
||||
if (product == null!)
|
||||
break;
|
||||
var capacity = _cargoConsoleSystem.GetCapacity(orders.Database.Id);
|
||||
if (
|
||||
capacity.CurrentCapacity == capacity.MaxCapacity
|
||||
|| capacity.CurrentCapacity + order.Amount > capacity.MaxCapacity
|
||||
|| !_cargoConsoleSystem.CheckBalance(_bankAccount.Id, (-product.PointCost) * order.Amount)
|
||||
|| !_cargoConsoleSystem.ApproveOrder(orders.Database.Id, msg.OrderNumber)
|
||||
|| !_cargoConsoleSystem.ChangeBalance(_bankAccount.Id, (-product.PointCost) * order.Amount)
|
||||
)
|
||||
{
|
||||
SoundSystem.Play(Filter.Local(), "/Audio/Effects/error.ogg", Owner, AudioParams.Default);
|
||||
break;
|
||||
}
|
||||
UpdateUIState();
|
||||
break;
|
||||
}
|
||||
case CargoConsoleShuttleMessage _:
|
||||
{
|
||||
//var approvedOrders = _cargoOrderDataManager.RemoveAndGetApprovedFrom(orders.Database);
|
||||
//orders.Database.ClearOrderCapacity();
|
||||
|
||||
// TODO replace with shuttle code
|
||||
// TEMPORARY loop for spawning stuff on telepad (looks for a telepad adjacent to the console)
|
||||
IEntity? cargoTelepad = null;
|
||||
var indices = Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, _mapManager);
|
||||
var offsets = new Vector2i[] { new Vector2i(0, 1), new Vector2i(1, 1), new Vector2i(1, 0), new Vector2i(1, -1),
|
||||
new Vector2i(0, -1), new Vector2i(-1, -1), new Vector2i(-1, 0), new Vector2i(-1, 1), };
|
||||
var adjacentEntities = new List<IEnumerable<IEntity>>(); //Probably better than IEnumerable.concat
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
adjacentEntities.Add((indices+offset).GetEntitiesInTileFast(Owner.Transform.GridID));
|
||||
}
|
||||
|
||||
foreach (var enumerator in adjacentEntities)
|
||||
{
|
||||
foreach (IEntity entity in enumerator)
|
||||
{
|
||||
if (entity.HasComponent<CargoTelepadComponent>() && entity.TryGetComponent<PowerReceiverComponent>(out var powerReceiver) && powerReceiver.Powered)
|
||||
{
|
||||
cargoTelepad = entity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cargoTelepad != null)
|
||||
{
|
||||
if (cargoTelepad.TryGetComponent<CargoTelepadComponent>(out var telepadComponent))
|
||||
{
|
||||
var approvedOrders = _cargoConsoleSystem.RemoveAndGetApprovedOrders(orders.Database.Id);
|
||||
orders.Database.ClearOrderCapacity();
|
||||
foreach (var order in approvedOrders)
|
||||
{
|
||||
if (!PrototypeManager.TryIndex(order.ProductId, out CargoProductPrototype? product))
|
||||
continue;
|
||||
for (var i = 0; i < order.Amount; i++)
|
||||
{
|
||||
telepadComponent.QueueTeleport(product);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!Powered)
|
||||
return;
|
||||
|
||||
UserInterface?.Open(actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void UpdateUIState()
|
||||
{
|
||||
if (_bankAccount == null || !Owner.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var id = _bankAccount.Id;
|
||||
var name = _bankAccount.Name;
|
||||
var balance = _bankAccount.Balance;
|
||||
var capacity = _cargoConsoleSystem.GetCapacity(id);
|
||||
UserInterface?.SetState(new CargoConsoleInterfaceState(_requestOnly, id, name, balance, capacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Content.Server.Cargo;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Components.Cargo;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Cargo
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CargoOrderDatabaseComponent : SharedCargoOrderDatabaseComponent
|
||||
{
|
||||
public CargoOrderDatabase? Database { get; set; }
|
||||
public bool ConnectedToDatabase => Database != null;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Database = EntitySystem.Get<CargoConsoleSystem>().StationOrderDatabase;
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
if (!ConnectedToDatabase)
|
||||
return new CargoOrderDatabaseState(null);
|
||||
return new CargoOrderDatabaseState(Database?.GetOrders());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Shared.Prototypes.Cargo;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Cargo
|
||||
{
|
||||
|
||||
//This entire class is a PLACEHOLDER for the cargo shuttle.
|
||||
|
||||
[RegisterComponent]
|
||||
public class CargoTelepadComponent : Component
|
||||
{
|
||||
public override string Name => "CargoTelepad";
|
||||
|
||||
private const float TeleportDuration = 0.5f;
|
||||
private const float TeleportDelay = 15f;
|
||||
private List<CargoProductPrototype> _teleportQueue = new List<CargoProductPrototype>();
|
||||
private CargoTelepadState _currentState = CargoTelepadState.Unpowered;
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
PowerUpdate(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void QueueTeleport(CargoProductPrototype product)
|
||||
{
|
||||
_teleportQueue.Add(product);
|
||||
TeleportLoop();
|
||||
}
|
||||
|
||||
private void PowerUpdate(PowerChangedMessage args)
|
||||
{
|
||||
if (args.Powered && _currentState == CargoTelepadState.Unpowered) {
|
||||
_currentState = CargoTelepadState.Idle;
|
||||
if(Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
|
||||
spriteComponent.LayerSetState(0, "idle");
|
||||
TeleportLoop();
|
||||
}
|
||||
else if (!args.Powered)
|
||||
{
|
||||
_currentState = CargoTelepadState.Unpowered;
|
||||
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
|
||||
spriteComponent.LayerSetState(0, "offline");
|
||||
}
|
||||
}
|
||||
private void TeleportLoop()
|
||||
{
|
||||
if (_currentState == CargoTelepadState.Idle && _teleportQueue.Count > 0)
|
||||
{
|
||||
_currentState = CargoTelepadState.Charging;
|
||||
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
|
||||
spriteComponent.LayerSetState(0, "idle");
|
||||
Owner.SpawnTimer((int) (TeleportDelay * 1000), () =>
|
||||
{
|
||||
if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Charging && _teleportQueue.Count > 0)
|
||||
{
|
||||
_currentState = CargoTelepadState.Teleporting;
|
||||
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
|
||||
spriteComponent.LayerSetState(0, "beam");
|
||||
Owner.SpawnTimer((int) (TeleportDuration * 1000), () =>
|
||||
{
|
||||
if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Teleporting && _teleportQueue.Count > 0)
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/phasein.ogg", Owner, AudioParams.Default.WithVolume(-8f));
|
||||
Owner.EntityManager.SpawnEntity(_teleportQueue[0].Product, Owner.Transform.Coordinates);
|
||||
_teleportQueue.RemoveAt(0);
|
||||
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
|
||||
spriteComponent.LayerSetState(0, "idle");
|
||||
_currentState = CargoTelepadState.Idle;
|
||||
TeleportLoop();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private enum CargoTelepadState { Unpowered, Idle, Charging, Teleporting };
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using Content.Shared.GameObjects.Components.Cargo;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Cargo
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class GalacticMarketComponent : SharedGalacticMarketComponent
|
||||
{
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
return new GalacticMarketState(GetProductIdList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Eui;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[GlobalVerb]
|
||||
internal sealed class AdminAddReagentVerb : GlobalVerb
|
||||
{
|
||||
public override bool RequireInteractionRange => false;
|
||||
public override bool BlockedByContainers => false;
|
||||
|
||||
private const AdminFlags ReqFlags = AdminFlags.Fun;
|
||||
|
||||
private static void OpenAddReagentMenu(IPlayerSession player, IEntity target)
|
||||
{
|
||||
var euiMgr = IoCManager.Resolve<EuiManager>();
|
||||
euiMgr.OpenEui(new AdminAddReagentEui(target), player);
|
||||
}
|
||||
|
||||
public override void GetData(IEntity user, IEntity target, VerbData data)
|
||||
{
|
||||
// ISolutionInteractionsComponent doesn't exactly have an interface for "admin tries to refill this", so...
|
||||
// Still have a path for SolutionContainerComponent in case it doesn't allow direct refilling.
|
||||
if (!target.HasComponent<SolutionContainerComponent>()
|
||||
&& !(target.TryGetComponent(out ISolutionInteractionsComponent? interactions)
|
||||
&& interactions.CanInject))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Add Reagent...");
|
||||
data.CategoryData = VerbCategories.Debug;
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
|
||||
var adminManager = IoCManager.Resolve<IAdminManager>();
|
||||
|
||||
if (user.TryGetComponent<ActorComponent>(out var player))
|
||||
{
|
||||
if (adminManager.HasAdminFlag(player.PlayerSession, ReqFlags))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Activate(IEntity user, IEntity target)
|
||||
{
|
||||
var groupController = IoCManager.Resolve<IAdminManager>();
|
||||
if (user.TryGetComponent<ActorComponent>(out var player))
|
||||
{
|
||||
if (groupController.HasAdminFlag(player.PlayerSession, ReqFlags))
|
||||
OpenAddReagentMenu(player.PlayerSession, target);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class AdminAddReagentEui : BaseEui
|
||||
{
|
||||
private readonly IEntity _target;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
|
||||
public AdminAddReagentEui(IEntity target)
|
||||
{
|
||||
_target = target;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
StateDirty();
|
||||
}
|
||||
|
||||
public override EuiStateBase GetNewState()
|
||||
{
|
||||
if (_target.TryGetComponent(out SolutionContainerComponent? container))
|
||||
{
|
||||
return new AdminAddReagentEuiState
|
||||
{
|
||||
CurVolume = container.CurrentVolume,
|
||||
MaxVolume = container.MaxVolume
|
||||
};
|
||||
}
|
||||
|
||||
if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions))
|
||||
{
|
||||
return new AdminAddReagentEuiState
|
||||
{
|
||||
// We don't exactly have an absolute total volume so good enough.
|
||||
CurVolume = ReagentUnit.Zero,
|
||||
MaxVolume = interactions.InjectSpaceAvailable
|
||||
};
|
||||
}
|
||||
|
||||
return new AdminAddReagentEuiState
|
||||
{
|
||||
CurVolume = ReagentUnit.Zero,
|
||||
MaxVolume = ReagentUnit.Zero
|
||||
};
|
||||
}
|
||||
|
||||
public override void HandleMessage(EuiMessageBase msg)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case AdminAddReagentEuiMsg.Close:
|
||||
Close();
|
||||
break;
|
||||
case AdminAddReagentEuiMsg.DoAdd doAdd:
|
||||
// Double check that user wasn't de-adminned in the mean time...
|
||||
// Or the target was deleted.
|
||||
if (!_adminManager.HasAdminFlag(Player, ReqFlags) || _target.Deleted)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var id = doAdd.ReagentId;
|
||||
var amount = doAdd.Amount;
|
||||
|
||||
if (_target.TryGetComponent(out SolutionContainerComponent? container))
|
||||
{
|
||||
container.TryAddReagent(id, amount, out _);
|
||||
}
|
||||
else if (_target.TryGetComponent(out ISolutionInteractionsComponent? interactions))
|
||||
{
|
||||
var solution = new Solution(id, amount);
|
||||
interactions.Inject(solution);
|
||||
}
|
||||
|
||||
StateDirty();
|
||||
|
||||
if (doAdd.CloseAfter)
|
||||
Close();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry.ChemMaster;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains all the server-side logic for chem masters. See also <see cref="SharedChemMasterComponent"/>.
|
||||
/// This includes initializing the component based on prototype data, and sending and receiving messages from the client.
|
||||
/// Messages sent to the client are used to update update the user interface for a component instance.
|
||||
/// Messages sent from the client are used to handle ui button presses.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
[ComponentReference(typeof(IInteractUsing))]
|
||||
public class ChemMasterComponent : SharedChemMasterComponent, IActivate, IInteractUsing, ISolutionChange
|
||||
{
|
||||
[ViewVariables] private ContainerSlot _beakerContainer = default!;
|
||||
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
|
||||
[ViewVariables] private bool _bufferModeTransfer = true;
|
||||
|
||||
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
|
||||
|
||||
[ViewVariables] private readonly Solution BufferSolution = new();
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ChemMasterUiKey.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Called once per instance of this component. Gets references to any other components needed
|
||||
/// by this component and initializes it's UI and other data.
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
|
||||
}
|
||||
|
||||
_beakerContainer =
|
||||
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-reagentContainerContainer");
|
||||
|
||||
//BufferSolution = Owner.BufferSolution
|
||||
BufferSolution.RemoveAllSolution();
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
OnPowerChanged(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPowerChanged(PowerChangedMessage e)
|
||||
{
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ui messages from the client. For things such as button presses
|
||||
/// which interact with the world and require server action.
|
||||
/// </summary>
|
||||
/// <param name="obj">A user interface message from the client.</param>
|
||||
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
if (obj.Session.AttachedEntity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = (UiActionMessage) obj.Message;
|
||||
var needsPower = msg.action switch
|
||||
{
|
||||
UiAction.Eject => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if (!PlayerCanUseChemMaster(obj.Session.AttachedEntity, needsPower))
|
||||
return;
|
||||
|
||||
switch (msg.action)
|
||||
{
|
||||
case UiAction.Eject:
|
||||
TryEject(obj.Session.AttachedEntity);
|
||||
break;
|
||||
case UiAction.ChemButton:
|
||||
TransferReagent(msg.id, msg.amount, msg.isBuffer);
|
||||
break;
|
||||
case UiAction.Transfer:
|
||||
_bufferModeTransfer = true;
|
||||
UpdateUserInterface();
|
||||
break;
|
||||
case UiAction.Discard:
|
||||
_bufferModeTransfer = false;
|
||||
UpdateUserInterface();
|
||||
break;
|
||||
case UiAction.CreatePills:
|
||||
case UiAction.CreateBottles:
|
||||
TryCreatePackage(obj.Session.AttachedEntity, msg.action, msg.pillAmount, msg.bottleAmount);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
ClickSound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the player entity is able to use the chem master.
|
||||
/// </summary>
|
||||
/// <param name="playerEntity">The player entity.</param>
|
||||
/// <returns>Returns true if the entity can use the chem master, and false if it cannot.</returns>
|
||||
private bool PlayerCanUseChemMaster(IEntity? playerEntity, bool needsPower = true)
|
||||
{
|
||||
//Need player entity to check if they are still able to use the chem master
|
||||
if (playerEntity == null)
|
||||
return false;
|
||||
//Check if player can interact in their current state
|
||||
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
|
||||
return false;
|
||||
//Check if device is powered
|
||||
if (needsPower && !Powered)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets component data to be used to update the user interface client-side.
|
||||
/// </summary>
|
||||
/// <returns>Returns a <see cref="SharedChemMasterComponent.ChemMasterBoundUserInterfaceState"/></returns>
|
||||
private ChemMasterBoundUserInterfaceState GetUserInterfaceState()
|
||||
{
|
||||
var beaker = _beakerContainer.ContainedEntity;
|
||||
if (beaker == null)
|
||||
{
|
||||
return new ChemMasterBoundUserInterfaceState(Powered, false, ReagentUnit.New(0), ReagentUnit.New(0),
|
||||
"", Owner.Name, new List<Solution.ReagentQuantity>(), BufferSolution.Contents, _bufferModeTransfer, BufferSolution.TotalVolume);
|
||||
}
|
||||
|
||||
var solution = beaker.GetComponent<SolutionContainerComponent>();
|
||||
return new ChemMasterBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
|
||||
beaker.Name, Owner.Name, solution.ReagentList, BufferSolution.Contents, _bufferModeTransfer, BufferSolution.TotalVolume);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface()
|
||||
{
|
||||
var state = GetUserInterfaceState();
|
||||
UserInterface?.SetState(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
|
||||
/// Tries to eject into user's hands first, then ejects onto chem master if both hands are full.
|
||||
/// </summary>
|
||||
private void TryEject(IEntity user)
|
||||
{
|
||||
if (!HasBeaker)
|
||||
return;
|
||||
|
||||
var beaker = _beakerContainer.ContainedEntity;
|
||||
|
||||
if(beaker is null)
|
||||
return;
|
||||
|
||||
_beakerContainer.Remove(beaker);
|
||||
UpdateUserInterface();
|
||||
|
||||
if(!user.TryGetComponent<HandsComponent>(out var hands) || !beaker.TryGetComponent<ItemComponent>(out var item))
|
||||
return;
|
||||
if (hands.CanPutInHand(item))
|
||||
hands.PutInHand(item);
|
||||
}
|
||||
|
||||
private void TransferReagent(string id, ReagentUnit amount, bool isBuffer)
|
||||
{
|
||||
if (!HasBeaker && _bufferModeTransfer) return;
|
||||
var beaker = _beakerContainer.ContainedEntity;
|
||||
|
||||
if(beaker is null)
|
||||
return;
|
||||
|
||||
var beakerSolution = beaker.GetComponent<SolutionContainerComponent>();
|
||||
if (isBuffer)
|
||||
{
|
||||
foreach (var reagent in BufferSolution.Contents)
|
||||
{
|
||||
if (reagent.ReagentId == id)
|
||||
{
|
||||
ReagentUnit actualAmount;
|
||||
if (amount == ReagentUnit.New(-1)) //amount is ReagentUnit.New(-1) when the client sends a message requesting to remove all solution from the container
|
||||
{
|
||||
actualAmount = ReagentUnit.Min(reagent.Quantity, beakerSolution.EmptyVolume);
|
||||
}
|
||||
else
|
||||
{
|
||||
actualAmount = ReagentUnit.Min(reagent.Quantity, amount, beakerSolution.EmptyVolume);
|
||||
}
|
||||
|
||||
|
||||
BufferSolution.RemoveReagent(id, actualAmount);
|
||||
if (_bufferModeTransfer)
|
||||
{
|
||||
beakerSolution.TryAddReagent(id, actualAmount, out var _);
|
||||
// beakerSolution.Solution.AddReagent(id, actualAmount);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var reagent in beakerSolution.Solution.Contents)
|
||||
{
|
||||
if (reagent.ReagentId == id)
|
||||
{
|
||||
ReagentUnit actualAmount;
|
||||
if (amount == ReagentUnit.New(-1))
|
||||
{
|
||||
actualAmount = reagent.Quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
actualAmount = ReagentUnit.Min(reagent.Quantity, amount);
|
||||
}
|
||||
beakerSolution.TryRemoveReagent(id, actualAmount);
|
||||
BufferSolution.AddReagent(id, actualAmount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
private void TryCreatePackage(IEntity user, UiAction action, int pillAmount, int bottleAmount)
|
||||
{
|
||||
if (BufferSolution.TotalVolume == 0)
|
||||
return;
|
||||
|
||||
if (action == UiAction.CreateBottles)
|
||||
{
|
||||
var individualVolume = BufferSolution.TotalVolume / ReagentUnit.New(bottleAmount);
|
||||
if (individualVolume < ReagentUnit.New(1))
|
||||
return;
|
||||
|
||||
var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(30));
|
||||
for (int i = 0; i < bottleAmount; i++)
|
||||
{
|
||||
var bottle = Owner.EntityManager.SpawnEntity("ChemistryEmptyBottle01", Owner.Transform.Coordinates);
|
||||
|
||||
var bufferSolution = BufferSolution.SplitSolution(actualVolume);
|
||||
|
||||
bottle.TryGetComponent<SolutionContainerComponent>(out var bottleSolution);
|
||||
bottleSolution?.TryAddSolution(bufferSolution);
|
||||
|
||||
//Try to give them the bottle
|
||||
if (user.TryGetComponent<HandsComponent>(out var hands) &&
|
||||
bottle.TryGetComponent<ItemComponent>(out var item))
|
||||
{
|
||||
if (hands.CanPutInHand(item))
|
||||
{
|
||||
hands.PutInHand(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
//Put it on the floor
|
||||
bottle.Transform.Coordinates = user.Transform.Coordinates;
|
||||
//Give it an offset
|
||||
bottle.RandomOffset(0.2f);
|
||||
}
|
||||
|
||||
}
|
||||
else //Pills
|
||||
{
|
||||
var individualVolume = BufferSolution.TotalVolume / ReagentUnit.New(pillAmount);
|
||||
if (individualVolume < ReagentUnit.New(1))
|
||||
return;
|
||||
|
||||
var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(50));
|
||||
for (int i = 0; i < pillAmount; i++)
|
||||
{
|
||||
var pill = Owner.EntityManager.SpawnEntity("pill", Owner.Transform.Coordinates);
|
||||
|
||||
var bufferSolution = BufferSolution.SplitSolution(actualVolume);
|
||||
|
||||
pill.TryGetComponent<SolutionContainerComponent>(out var pillSolution);
|
||||
pillSolution?.TryAddSolution(bufferSolution);
|
||||
|
||||
//Try to give them the bottle
|
||||
if (user.TryGetComponent<HandsComponent>(out var hands) &&
|
||||
pill.TryGetComponent<ItemComponent>(out var item))
|
||||
{
|
||||
if (hands.CanPutInHand(item))
|
||||
{
|
||||
hands.PutInHand(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Put it on the floor
|
||||
pill.Transform.Coordinates = user.Transform.Coordinates;
|
||||
//Give it an offset
|
||||
pill.RandomOffset(0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
|
||||
/// </summary>
|
||||
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
|
||||
void IActivate.Activate(ActivateEventArgs args)
|
||||
{
|
||||
if (!args.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.User.TryGetComponent(out IHandsComponent? hands))
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
|
||||
return;
|
||||
}
|
||||
|
||||
var activeHandEntity = hands.GetActiveHand?.Owner;
|
||||
if (activeHandEntity == null)
|
||||
{
|
||||
UserInterface?.Open(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
|
||||
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the chem master doesn't already
|
||||
/// hold a container, it will be added to the chem master.
|
||||
/// </summary>
|
||||
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
|
||||
/// <returns></returns>
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
|
||||
{
|
||||
if (!args.User.TryGetComponent(out IHandsComponent? hands))
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You have no hands!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hands.GetActiveHand == null)
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You have nothing in your hand!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var activeHandEntity = hands.GetActiveHand.Owner;
|
||||
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
|
||||
{
|
||||
if (HasBeaker)
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("This ChemMaster already has a container in it."));
|
||||
}
|
||||
else if (!solution.CanUseWithChemDispenser)
|
||||
{
|
||||
//If it can't fit in the chem master, don't put it in. For example, buckets and mop buckets can't fit.
|
||||
Owner.PopupMessage(args.User, Loc.GetString("The {0:theName} is too large for the ChemMaster!", activeHandEntity));
|
||||
}
|
||||
else
|
||||
{
|
||||
_beakerContainer.Insert(activeHandEntity);
|
||||
UpdateUserInterface();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You can't put {0:theName} in the ChemMaster!", activeHandEntity));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => UpdateUserInterface();
|
||||
|
||||
private void ClickSound()
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
|
||||
[Verb]
|
||||
public sealed class EjectBeakerVerb : Verb<ChemMasterComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ChemMasterComponent component, VerbData data)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Eject Beaker");
|
||||
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ChemMasterComponent component)
|
||||
{
|
||||
component.TryEject(user);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SolutionAreaEffectComponent))]
|
||||
public class FoamSolutionAreaEffectComponent : SolutionAreaEffectComponent
|
||||
{
|
||||
public override string Name => "FoamSolutionAreaEffect";
|
||||
|
||||
[DataField("foamedMetalPrototype")] private string? _foamedMetalPrototype;
|
||||
|
||||
protected override void UpdateVisuals()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
|
||||
SolutionContainerComponent != null)
|
||||
{
|
||||
appearance.SetData(FoamVisuals.Color, SolutionContainerComponent.Color.WithAlpha(0.80f));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
|
||||
{
|
||||
if (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
return;
|
||||
|
||||
// TODO: Add a permeability property to clothing
|
||||
// For now it just adds to protection for each clothing equipped
|
||||
var protection = 0f;
|
||||
if (entity.TryGetComponent(out InventoryComponent? inventory))
|
||||
{
|
||||
foreach (var slot in inventory.Slots)
|
||||
{
|
||||
if (slot == EquipmentSlotDefines.Slots.BACKPACK ||
|
||||
slot == EquipmentSlotDefines.Slots.POCKET1 ||
|
||||
slot == EquipmentSlotDefines.Slots.POCKET2 ||
|
||||
slot == EquipmentSlotDefines.Slots.IDCARD)
|
||||
continue;
|
||||
|
||||
if (inventory.TryGetSlotItem(slot, out ItemComponent _))
|
||||
protection += 0.025f;
|
||||
}
|
||||
}
|
||||
|
||||
var cloneSolution = SolutionContainerComponent.Solution.Clone();
|
||||
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction * (1 - protection), bloodstream.EmptyVolume);
|
||||
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
||||
|
||||
bloodstream.TryTransferSolution(transferSolution);
|
||||
}
|
||||
|
||||
protected override void OnKill()
|
||||
{
|
||||
if (Owner.Deleted)
|
||||
return;
|
||||
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
|
||||
{
|
||||
appearance.SetData(FoamVisuals.State, true);
|
||||
}
|
||||
Owner.SpawnTimer(600, () =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_foamedMetalPrototype))
|
||||
{
|
||||
Owner.EntityManager.SpawnEntity(_foamedMetalPrototype, Owner.Transform.Coordinates);
|
||||
}
|
||||
Owner.QueueDelete();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Mobs.State;
|
||||
using Content.Server.GameObjects.EntitySystems.Weapon.Melee;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class HyposprayComponent : SharedHyposprayComponent, ISolutionChange
|
||||
{
|
||||
[DataField("ClumsyFailChance")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float ClumsyFailChance { get; set; } = 0.5f;
|
||||
|
||||
[DataField("TransferAmount")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(5);
|
||||
|
||||
[ComponentDependency] private readonly SolutionContainerComponent? _solution = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public bool TryDoInject(IEntity? target, IEntity user)
|
||||
{
|
||||
if (target == null || !EligibleEntity(target))
|
||||
return false;
|
||||
|
||||
var msgFormat = "You inject {0:TheName}.";
|
||||
|
||||
if (target == user)
|
||||
{
|
||||
msgFormat = "You inject yourself.";
|
||||
}
|
||||
else if (EligibleEntity(user) && ClumsyComponent.TryRollClumsy(user, ClumsyFailChance))
|
||||
{
|
||||
msgFormat = "Oops! You injected yourself!";
|
||||
target = user;
|
||||
}
|
||||
|
||||
if (_solution == null || _solution.CurrentVolume == 0)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("It's empty!"));
|
||||
return true;
|
||||
}
|
||||
|
||||
user.PopupMessage(Loc.GetString(msgFormat, target));
|
||||
if (target != user)
|
||||
{
|
||||
target.PopupMessage(Loc.GetString("You feel a tiny prick!"));
|
||||
var meleeSys = EntitySystem.Get<MeleeWeaponSystem>();
|
||||
var angle = Angle.FromWorldVec(target.Transform.WorldPosition - user.Transform.WorldPosition);
|
||||
meleeSys.SendLunge(angle, user);
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(user), "/Audio/Items/hypospray.ogg", user);
|
||||
|
||||
var targetSolution = target.GetComponent<SolutionContainerComponent>();
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = ReagentUnit.Min(TransferAmount, targetSolution.EmptyVolume);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
user.PopupMessage(user, Loc.GetString("{0:TheName} is already full!", targetSolution.Owner));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = _solution.SplitSolution(realTransferAmount);
|
||||
|
||||
if (!targetSolution.CanAddSolution(removedSolution))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
removedSolution.DoEntityReaction(target, ReactionMethod.Injection);
|
||||
|
||||
targetSolution.TryAddSolution(removedSolution);
|
||||
|
||||
static bool EligibleEntity(IEntity entity)
|
||||
{
|
||||
// TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag?
|
||||
// In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else.
|
||||
return entity.HasComponent<SolutionContainerComponent>() && entity.HasComponent<MobStateComponent>();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
|
||||
{
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
if (_solution == null)
|
||||
return new HyposprayComponentState(ReagentUnit.Zero, ReagentUnit.Zero);
|
||||
|
||||
return new HyposprayComponentState(_solution.CurrentVolume, _solution.MaxVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Server behavior for reagent injectors and syringes. Can optionally support both
|
||||
/// injection and drawing or just injection. Can inject/draw reagents from solution
|
||||
/// containers, and can directly inject into a mobs bloodstream.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class InjectorComponent : SharedInjectorComponent, IAfterInteract, IUse, ISolutionChange
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not the injector is able to draw from containers or if it's a single use
|
||||
/// device that can only inject.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("injectOnly")]
|
||||
private bool _injectOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Amount to inject or draw on each usage. If the injector is inject only, it will
|
||||
/// attempt to inject it's entire contents upon use.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("transferAmount")]
|
||||
private ReagentUnit _transferAmount = ReagentUnit.New(5);
|
||||
|
||||
/// <summary>
|
||||
/// Initial storage volume of the injector
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("initialMaxVolume")]
|
||||
private ReagentUnit _initialMaxVolume = ReagentUnit.New(15);
|
||||
|
||||
private InjectorToggleMode _toggleState;
|
||||
|
||||
/// <summary>
|
||||
/// The state of the injector. Determines it's attack behavior. Containers must have the
|
||||
/// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
|
||||
/// only ever be set to Inject
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public InjectorToggleMode ToggleState
|
||||
{
|
||||
get => _toggleState;
|
||||
set
|
||||
{
|
||||
_toggleState = value;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle between draw/inject state if applicable
|
||||
/// </summary>
|
||||
private void Toggle(IEntity user)
|
||||
{
|
||||
if (_injectOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string msg;
|
||||
switch (ToggleState)
|
||||
{
|
||||
case InjectorToggleMode.Inject:
|
||||
ToggleState = InjectorToggleMode.Draw;
|
||||
msg = "Now drawing";
|
||||
break;
|
||||
case InjectorToggleMode.Draw:
|
||||
ToggleState = InjectorToggleMode.Inject;
|
||||
msg = "Now injecting";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user, Loc.GetString(msg));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when clicking on entities while holding in active hand
|
||||
/// </summary>
|
||||
/// <param name="eventArgs"></param>
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true))
|
||||
return false;
|
||||
|
||||
//Make sure we have the attacking entity
|
||||
if (eventArgs.Target == null || !Owner.HasComponent<SolutionContainerComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetEntity = eventArgs.Target;
|
||||
|
||||
// Handle injecting/drawing for solutions
|
||||
if (targetEntity.TryGetComponent<ISolutionInteractionsComponent>(out var targetSolution))
|
||||
{
|
||||
if (ToggleState == InjectorToggleMode.Inject)
|
||||
{
|
||||
if (targetSolution.CanInject)
|
||||
{
|
||||
TryInject(targetSolution, eventArgs.User);
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User,
|
||||
Loc.GetString("You aren't able to transfer to {0:theName}!", targetSolution.Owner));
|
||||
}
|
||||
}
|
||||
else if (ToggleState == InjectorToggleMode.Draw)
|
||||
{
|
||||
if (targetSolution.CanDraw)
|
||||
{
|
||||
TryDraw(targetSolution, eventArgs.User);
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User,
|
||||
Loc.GetString("You aren't able to draw from {0:theName}!", targetSolution.Owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle injecting into bloodstream
|
||||
else if (targetEntity.TryGetComponent(out BloodstreamComponent? bloodstream) &&
|
||||
ToggleState == InjectorToggleMode.Inject)
|
||||
{
|
||||
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when use key is pressed when held in active hand
|
||||
/// </summary>
|
||||
/// <param name="eventArgs"></param>
|
||||
/// <returns></returns>
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
Toggle(eventArgs.User);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, IEntity user)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetBloodstream.EmptyVolume);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("You aren't able to inject {0:theName}!", targetBloodstream.Owner));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = solution.SplitSolution(realTransferAmount);
|
||||
|
||||
if (!solution.CanAddSolution(removedSolution))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Account for partial transfer.
|
||||
|
||||
removedSolution.DoEntityReaction(solution.Owner, ReactionMethod.Injection);
|
||||
|
||||
solution.TryAddSolution(removedSolution);
|
||||
|
||||
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
|
||||
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("You inject {0}u into {1:theName}!", removedSolution.TotalVolume,
|
||||
targetBloodstream.Owner));
|
||||
Dirty();
|
||||
AfterInject();
|
||||
}
|
||||
|
||||
private void TryInject(ISolutionInteractionsComponent targetSolution, IEntity user)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.CurrentVolume == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.InjectSpaceAvailable);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("{0:theName} is already full!", targetSolution.Owner));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = solution.SplitSolution(realTransferAmount);
|
||||
|
||||
removedSolution.DoEntityReaction(targetSolution.Owner, ReactionMethod.Injection);
|
||||
|
||||
targetSolution.Inject(removedSolution);
|
||||
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("You transfer {0}u to {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
|
||||
Dirty();
|
||||
AfterInject();
|
||||
}
|
||||
|
||||
private void AfterInject()
|
||||
{
|
||||
// Automatically set syringe to draw after completely draining it.
|
||||
if (Owner.GetComponent<SolutionContainerComponent>().CurrentVolume == 0)
|
||||
{
|
||||
ToggleState = InjectorToggleMode.Draw;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryDraw(ISolutionInteractionsComponent targetSolution, IEntity user)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || solution.EmptyVolume == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = ReagentUnit.Min(_transferAmount, targetSolution.DrawAvailable);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
Owner.PopupMessage(user, Loc.GetString("{0:theName} is empty!", targetSolution.Owner));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = targetSolution.Draw(realTransferAmount);
|
||||
|
||||
if (!solution.TryAddSolution(removedSolution))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("Drew {0}u from {1:theName}", removedSolution.TotalVolume, targetSolution.Owner));
|
||||
Dirty();
|
||||
AfterDraw();
|
||||
}
|
||||
|
||||
private void AfterDraw()
|
||||
{
|
||||
// Automatically set syringe to inject after completely filling it.
|
||||
if (Owner.GetComponent<SolutionContainerComponent>().EmptyVolume == 0)
|
||||
{
|
||||
ToggleState = InjectorToggleMode.Inject;
|
||||
}
|
||||
}
|
||||
|
||||
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
|
||||
{
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
Owner.TryGetComponent(out SolutionContainerComponent? solution);
|
||||
|
||||
var currentVolume = solution?.CurrentVolume ?? ReagentUnit.Zero;
|
||||
var maxVolume = solution?.MaxVolume ?? ReagentUnit.Zero;
|
||||
|
||||
return new InjectorComponentState(currentVolume, maxVolume, ToggleState);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Body.Behavior;
|
||||
using Content.Server.GameObjects.Components.Culinary;
|
||||
using Content.Server.GameObjects.Components.Nutrition;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class PillComponent : FoodComponent, IUse, IAfterInteract
|
||||
{
|
||||
public override string Name => "Pill";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("useSound")]
|
||||
protected override string? UseSound { get; set; } = default;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("trash")]
|
||||
protected override string? TrashPrototype { get; set; } = default;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("transferAmount")]
|
||||
protected override ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(1000);
|
||||
|
||||
[ViewVariables]
|
||||
private SolutionContainerComponent _contents = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Owner.EnsureComponentWarn(out _contents);
|
||||
}
|
||||
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
return TryUseFood(eventArgs.User, null);
|
||||
}
|
||||
|
||||
// Feeding someone else
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TryUseFood(eventArgs.User, eventArgs.Target);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool TryUseFood(IEntity? user, IEntity? target, UtensilComponent? utensilUsed = null)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trueTarget = target ?? user;
|
||||
|
||||
if (!trueTarget.TryGetComponent(out IBody? body) ||
|
||||
!body.TryGetMechanismBehaviors<StomachBehavior>(out var stomachs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.InRangeUnobstructed(trueTarget, popup: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var transferAmount = ReagentUnit.Min(TransferAmount, _contents.CurrentVolume);
|
||||
var split = _contents.SplitSolution(transferAmount);
|
||||
|
||||
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!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Account for partial transfer.
|
||||
|
||||
split.DoEntityReaction(trueTarget, ReactionMethod.Ingestion);
|
||||
|
||||
firstStomach.TryTransferSolution(split);
|
||||
|
||||
if (UseSound != null)
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(trueTarget), UseSound, trueTarget, AudioParams.Default.WithVolume(-1f));
|
||||
}
|
||||
|
||||
trueTarget.PopupMessage(user, Loc.GetString("You swallow the pill."));
|
||||
|
||||
Owner.QueueDelete();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry.ReagentDispenser;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains all the server-side logic for reagent dispensers. See also <see cref="SharedReagentDispenserComponent"/>.
|
||||
/// This includes initializing the component based on prototype data, and sending and receiving messages from the client.
|
||||
/// Messages sent to the client are used to update update the user interface for a component instance.
|
||||
/// Messages sent from the client are used to handle ui button presses.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
[ComponentReference(typeof(IInteractUsing))]
|
||||
public class ReagentDispenserComponent : SharedReagentDispenserComponent, IActivate, IInteractUsing, ISolutionChange
|
||||
{
|
||||
private static ReagentInventoryComparer _comparer = new();
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[ViewVariables] private ContainerSlot _beakerContainer = default!;
|
||||
[ViewVariables] [DataField("pack")] private string _packPrototypeId = "";
|
||||
|
||||
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
|
||||
[ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10);
|
||||
[UsedImplicitly] [ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
|
||||
|
||||
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentDispenserUiKey.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Called once per instance of this component. Gets references to any other components needed
|
||||
/// by this component and initializes it's UI and other data.
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
|
||||
}
|
||||
|
||||
_beakerContainer =
|
||||
ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-reagentContainerContainer");
|
||||
|
||||
InitializeFromPrototype();
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
OnPowerChanged(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the <c>pack</c> defined in this components yaml prototype
|
||||
/// exists. If so, it fills the reagent inventory list.
|
||||
/// </summary>
|
||||
private void InitializeFromPrototype()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_packPrototypeId)) return;
|
||||
|
||||
if (!_prototypeManager.TryIndex(_packPrototypeId, out ReagentDispenserInventoryPrototype? packPrototype))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var entry in packPrototype.Inventory)
|
||||
{
|
||||
Inventory.Add(new ReagentDispenserInventoryEntry(entry));
|
||||
}
|
||||
|
||||
Inventory.Sort(_comparer);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(PowerChangedMessage e)
|
||||
{
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ui messages from the client. For things such as button presses
|
||||
/// which interact with the world and require server action.
|
||||
/// </summary>
|
||||
/// <param name="obj">A user interface message from the client.</param>
|
||||
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
if (obj.Session.AttachedEntity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = (UiButtonPressedMessage) obj.Message;
|
||||
var needsPower = msg.Button switch
|
||||
{
|
||||
UiButton.Eject => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if(!PlayerCanUseDispenser(obj.Session.AttachedEntity, needsPower))
|
||||
return;
|
||||
|
||||
switch (msg.Button)
|
||||
{
|
||||
case UiButton.Eject:
|
||||
TryEject(obj.Session.AttachedEntity);
|
||||
break;
|
||||
case UiButton.Clear:
|
||||
TryClear();
|
||||
break;
|
||||
case UiButton.SetDispenseAmount1:
|
||||
_dispenseAmount = ReagentUnit.New(1);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount5:
|
||||
_dispenseAmount = ReagentUnit.New(5);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount10:
|
||||
_dispenseAmount = ReagentUnit.New(10);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount15:
|
||||
_dispenseAmount = ReagentUnit.New(15);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount20:
|
||||
_dispenseAmount = ReagentUnit.New(20);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount25:
|
||||
_dispenseAmount = ReagentUnit.New(25);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount30:
|
||||
_dispenseAmount = ReagentUnit.New(30);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount50:
|
||||
_dispenseAmount = ReagentUnit.New(50);
|
||||
break;
|
||||
case UiButton.SetDispenseAmount100:
|
||||
_dispenseAmount = ReagentUnit.New(100);
|
||||
break;
|
||||
case UiButton.Dispense:
|
||||
if (HasBeaker)
|
||||
{
|
||||
TryDispense(msg.DispenseIndex);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
ClickSound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the player entity is able to use the chem dispenser.
|
||||
/// </summary>
|
||||
/// <param name="playerEntity">The player entity.</param>
|
||||
/// <returns>Returns true if the entity can use the dispenser, and false if it cannot.</returns>
|
||||
private bool PlayerCanUseDispenser(IEntity? playerEntity, bool needsPower = true)
|
||||
{
|
||||
//Need player entity to check if they are still able to use the dispenser
|
||||
if (playerEntity == null)
|
||||
return false;
|
||||
//Check if player can interact in their current state
|
||||
if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity))
|
||||
return false;
|
||||
//Check if device is powered
|
||||
if (needsPower && !Powered)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets component data to be used to update the user interface client-side.
|
||||
/// </summary>
|
||||
/// <returns>Returns a <see cref="SharedReagentDispenserComponent.ReagentDispenserBoundUserInterfaceState"/></returns>
|
||||
private ReagentDispenserBoundUserInterfaceState GetUserInterfaceState()
|
||||
{
|
||||
var beaker = _beakerContainer.ContainedEntity;
|
||||
if (beaker == null)
|
||||
{
|
||||
return new ReagentDispenserBoundUserInterfaceState(Powered, false, ReagentUnit.New(0), ReagentUnit.New(0),
|
||||
string.Empty, Inventory, Owner.Name, null, _dispenseAmount);
|
||||
}
|
||||
|
||||
var solution = beaker.GetComponent<SolutionContainerComponent>();
|
||||
return new ReagentDispenserBoundUserInterfaceState(Powered, true, solution.CurrentVolume, solution.MaxVolume,
|
||||
beaker.Name, Inventory, Owner.Name, solution.ReagentList.ToList(), _dispenseAmount);
|
||||
}
|
||||
|
||||
private void UpdateUserInterface()
|
||||
{
|
||||
var state = GetUserInterfaceState();
|
||||
UserInterface?.SetState(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, eject it.
|
||||
/// Tries to eject into user's hands first, then ejects onto dispenser if both hands are full.
|
||||
/// </summary>
|
||||
private void TryEject(IEntity user)
|
||||
{
|
||||
if (!HasBeaker)
|
||||
return;
|
||||
|
||||
var beaker = _beakerContainer.ContainedEntity;
|
||||
if(beaker is null)
|
||||
return;
|
||||
|
||||
_beakerContainer.Remove(beaker);
|
||||
UpdateUserInterface();
|
||||
|
||||
if(!user.TryGetComponent<HandsComponent>(out var hands) || !beaker.TryGetComponent<ItemComponent>(out var item))
|
||||
return;
|
||||
if (hands.CanPutInHand(item))
|
||||
hands.PutInHand(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, remove all of it's reagents / solutions.
|
||||
/// </summary>
|
||||
private void TryClear()
|
||||
{
|
||||
if (!HasBeaker) return;
|
||||
var solution = _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
|
||||
if(solution is null)
|
||||
return;
|
||||
|
||||
solution.RemoveAllSolution();
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If this component contains an entity with a <see cref="SolutionContainerComponent"/>, attempt to dispense the specified reagent to it.
|
||||
/// </summary>
|
||||
/// <param name="dispenseIndex">The index of the reagent in <c>Inventory</c>.</param>
|
||||
private void TryDispense(int dispenseIndex)
|
||||
{
|
||||
if (!HasBeaker) return;
|
||||
|
||||
var solution = _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
|
||||
if (solution is null)
|
||||
return;
|
||||
|
||||
solution.TryAddReagent(Inventory[dispenseIndex].ID, _dispenseAmount, out _);
|
||||
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible.
|
||||
/// </summary>
|
||||
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
|
||||
void IActivate.Activate(ActivateEventArgs args)
|
||||
{
|
||||
if (!args.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.User.TryGetComponent(out IHandsComponent? hands))
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
|
||||
return;
|
||||
}
|
||||
|
||||
var activeHandEntity = hands.GetActiveHand?.Owner;
|
||||
if (activeHandEntity == null)
|
||||
{
|
||||
UserInterface?.Open(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when you click the owner entity with something in your active hand. If the entity in your hand
|
||||
/// contains a <see cref="SolutionContainerComponent"/>, if you have hands, and if the dispenser doesn't already
|
||||
/// hold a container, it will be added to the dispenser.
|
||||
/// </summary>
|
||||
/// <param name="args">Data relevant to the event such as the actor which triggered it.</param>
|
||||
/// <returns></returns>
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs args)
|
||||
{
|
||||
if (!args.User.TryGetComponent(out IHandsComponent? hands))
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You have no hands."));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hands.GetActiveHand == null)
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You have nothing on your hand."));
|
||||
return false;
|
||||
}
|
||||
|
||||
var activeHandEntity = hands.GetActiveHand.Owner;
|
||||
if (activeHandEntity.TryGetComponent<SolutionContainerComponent>(out var solution))
|
||||
{
|
||||
if (HasBeaker)
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("This dispenser already has a container in it."));
|
||||
}
|
||||
else if ((solution.Capabilities & SolutionContainerCaps.FitsInDispenser) == 0)
|
||||
{
|
||||
//If it can't fit in the dispenser, don't put it in. For example, buckets and mop buckets can't fit.
|
||||
Owner.PopupMessage(args.User, Loc.GetString("That can't fit in the dispenser."));
|
||||
}
|
||||
else
|
||||
{
|
||||
_beakerContainer.Insert(activeHandEntity);
|
||||
UpdateUserInterface();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.PopupMessage(args.User, Loc.GetString("You can't put this in the dispenser."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => UpdateUserInterface();
|
||||
|
||||
private void ClickSound()
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
|
||||
[Verb]
|
||||
public sealed class EjectBeakerVerb : Verb<ReagentDispenserComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ReagentDispenserComponent component, VerbData data)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Eject Beaker");
|
||||
data.Visibility = component.HasBeaker ? VerbVisibility.Visible : VerbVisibility.Invisible;
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ReagentDispenserComponent component)
|
||||
{
|
||||
component.TryEject(user);
|
||||
}
|
||||
}
|
||||
|
||||
private class ReagentInventoryComparer : Comparer<ReagentDispenserInventoryEntry>
|
||||
{
|
||||
public override int Compare(ReagentDispenserInventoryEntry x, ReagentDispenserInventoryEntry y)
|
||||
{
|
||||
return string.Compare(x.ID, y.ID, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using Content.Shared.Chemistry;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ReagentTankComponent : Component
|
||||
{
|
||||
public override string Name => "ReagentTank";
|
||||
|
||||
[DataField("transferAmount")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(10);
|
||||
|
||||
[DataField("tankType")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public ReagentTankType TankType { get; set; } = ReagentTankType.Unspecified;
|
||||
}
|
||||
|
||||
public enum ReagentTankType : byte
|
||||
{
|
||||
Unspecified,
|
||||
Fuel
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Basically, monkey cubes.
|
||||
/// But specifically, this component deletes the entity and spawns in a new entity when the entity is exposed to a given reagent.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(ISolutionChange))]
|
||||
public class RehydratableComponent : Component, ISolutionChange
|
||||
{
|
||||
public override string Name => "Rehydratable";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("catalyst")]
|
||||
private string _catalystPrototype = "Water";
|
||||
[ViewVariables]
|
||||
[DataField("target")]
|
||||
private string? _targetPrototype = default!;
|
||||
|
||||
private bool _expanding;
|
||||
|
||||
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
|
||||
{
|
||||
var solution = eventArgs.Owner.GetComponent<SolutionContainerComponent>();
|
||||
if (solution.Solution.GetReagentQuantity(_catalystPrototype) > ReagentUnit.Zero)
|
||||
{
|
||||
Expand();
|
||||
}
|
||||
}
|
||||
|
||||
// Try not to make this public if you can help it.
|
||||
private void Expand()
|
||||
{
|
||||
if (_expanding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_expanding = true;
|
||||
Owner.PopupMessageEveryone(Loc.GetString("{0:TheName} expands!", Owner));
|
||||
if (!string.IsNullOrEmpty(_targetPrototype))
|
||||
{
|
||||
var ent = Owner.EntityManager.SpawnEntity(_targetPrototype, Owner.Transform.Coordinates);
|
||||
ent.Transform.AttachToGridOrMap();
|
||||
}
|
||||
Owner.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body.Circulatory;
|
||||
using Content.Server.GameObjects.Components.Body.Respiratory;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SolutionAreaEffectComponent))]
|
||||
public class SmokeSolutionAreaEffectComponent : SolutionAreaEffectComponent
|
||||
{
|
||||
public override string Name => "SmokeSolutionAreaEffect";
|
||||
|
||||
protected override void UpdateVisuals()
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent? appearance) &&
|
||||
SolutionContainerComponent != null)
|
||||
{
|
||||
appearance.SetData(SmokeVisuals.Color, SolutionContainerComponent.Color);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ReactWithEntity(IEntity entity, double solutionFraction)
|
||||
{
|
||||
if (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
if (!entity.TryGetComponent(out BloodstreamComponent? bloodstream))
|
||||
return;
|
||||
|
||||
if (entity.TryGetComponent(out InternalsComponent? internals) &&
|
||||
internals.AreInternalsWorking())
|
||||
return;
|
||||
|
||||
var chemistry = EntitySystem.Get<ChemistrySystem>();
|
||||
var cloneSolution = SolutionContainerComponent.Solution.Clone();
|
||||
var transferAmount = ReagentUnit.Min(cloneSolution.TotalVolume * solutionFraction, bloodstream.EmptyVolume);
|
||||
var transferSolution = cloneSolution.SplitSolution(transferAmount);
|
||||
|
||||
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
|
||||
{
|
||||
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
|
||||
chemistry.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity.ReagentId, reagentQuantity.Quantity, transferSolution);
|
||||
}
|
||||
|
||||
bloodstream.TryTransferSolution(transferSolution);
|
||||
}
|
||||
|
||||
|
||||
protected override void OnKill()
|
||||
{
|
||||
if (Owner.Deleted)
|
||||
return;
|
||||
Owner.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Atmos;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to clone its owner repeatedly and group up them all so they behave like one unit, that way you can have
|
||||
/// effects that cover an area. Inherited by <see cref="SmokeSolutionAreaEffectComponent"/> and <see cref="FoamSolutionAreaEffectComponent"/>.
|
||||
/// </summary>
|
||||
public abstract class SolutionAreaEffectComponent : Component
|
||||
{
|
||||
[Dependency] protected readonly IMapManager MapManager = default!;
|
||||
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
|
||||
|
||||
[ComponentDependency] protected readonly SolutionContainerComponent? SolutionContainerComponent = default!;
|
||||
public int Amount { get; set; }
|
||||
public SolutionAreaEffectInceptionComponent? Inception { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="SolutionAreaEffectInceptionComponent"/> to owner so the effect starts spreading and reacting.
|
||||
/// </summary>
|
||||
/// <param name="amount">The range of the effect</param>
|
||||
/// <param name="duration"></param>
|
||||
/// <param name="spreadDelay"></param>
|
||||
/// <param name="removeDelay"></param>
|
||||
public void Start(int amount, float duration, float spreadDelay, float removeDelay)
|
||||
{
|
||||
if (Inception != null)
|
||||
return;
|
||||
|
||||
if (Owner.HasComponent<SolutionAreaEffectInceptionComponent>())
|
||||
return;
|
||||
|
||||
Amount = amount;
|
||||
var inception = Owner.AddComponent<SolutionAreaEffectInceptionComponent>();
|
||||
|
||||
inception.Add(this);
|
||||
inception.Setup(amount, duration, spreadDelay, removeDelay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets called by an AreaEffectInceptionComponent. "Clones" Owner into the four directions and copies the
|
||||
/// solution into each of them.
|
||||
/// </summary>
|
||||
public void Spread()
|
||||
{
|
||||
if (Owner.Prototype == null)
|
||||
{
|
||||
Logger.Error("AreaEffectComponent needs its owner to be spawned by a prototype.");
|
||||
return;
|
||||
}
|
||||
|
||||
void SpreadToDir(Direction dir)
|
||||
{
|
||||
var grid = MapManager.GetGrid(Owner.Transform.GridID);
|
||||
var coords = Owner.Transform.Coordinates;
|
||||
foreach (var neighbor in grid.GetInDir(coords, dir))
|
||||
{
|
||||
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out SolutionAreaEffectComponent? comp) && comp.Inception == Inception)
|
||||
return;
|
||||
|
||||
if (Owner.EntityManager.ComponentManager.TryGetComponent(neighbor, out AirtightComponent? airtight) && airtight.AirBlocked)
|
||||
return;
|
||||
}
|
||||
|
||||
var newEffect = Owner.EntityManager.SpawnEntity(Owner.Prototype.ID, grid.DirectionToGrid(coords, dir));
|
||||
|
||||
if (!newEffect.TryGetComponent(out SolutionAreaEffectComponent? effectComponent))
|
||||
{
|
||||
newEffect.Delete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (SolutionContainerComponent != null)
|
||||
{
|
||||
effectComponent.TryAddSolution(SolutionContainerComponent.Solution.Clone());
|
||||
}
|
||||
|
||||
effectComponent.Amount = Amount - 1;
|
||||
Inception?.Add(effectComponent);
|
||||
}
|
||||
|
||||
SpreadToDir(Direction.North);
|
||||
SpreadToDir(Direction.East);
|
||||
SpreadToDir(Direction.South);
|
||||
SpreadToDir(Direction.West);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets called by an AreaEffectInceptionComponent.
|
||||
/// Removes this component from its inception and calls OnKill(). The implementation of OnKill() should
|
||||
/// eventually delete the entity.
|
||||
/// </summary>
|
||||
public void Kill()
|
||||
{
|
||||
Inception?.Remove(this);
|
||||
OnKill();
|
||||
}
|
||||
|
||||
protected abstract void OnKill();
|
||||
|
||||
/// <summary>
|
||||
/// Gets called by an AreaEffectInceptionComponent.
|
||||
/// Makes this effect's reagents react with the tile its on and with the entities it covers. Also calls
|
||||
/// ReactWithEntity on the entities so inheritors can implement more specific behavior.
|
||||
/// </summary>
|
||||
/// <param name="averageExposures">How many times will this get called over this area effect's duration, averaged
|
||||
/// with the other area effects from the inception.</param>
|
||||
public void React(float averageExposures)
|
||||
{
|
||||
if (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
var chemistry = EntitySystem.Get<ChemistrySystem>();
|
||||
var mapGrid = MapManager.GetGrid(Owner.Transform.GridID);
|
||||
var tile = mapGrid.GetTileRef(Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, MapManager));
|
||||
|
||||
var solutionFraction = 1 / Math.Floor(averageExposures);
|
||||
|
||||
foreach (var reagentQuantity in SolutionContainerComponent.ReagentList.ToArray())
|
||||
{
|
||||
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
|
||||
var reagent = PrototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
|
||||
|
||||
// React with the tile the effect is on
|
||||
reagent.ReactionTile(tile, reagentQuantity.Quantity * solutionFraction);
|
||||
|
||||
// Touch every entity on the tile
|
||||
foreach (var entity in tile.GetEntitiesInTileFast().ToArray())
|
||||
{
|
||||
chemistry.ReactionEntity(entity, ReactionMethod.Touch, reagent, reagentQuantity.Quantity * solutionFraction, SolutionContainerComponent.Solution);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entity in tile.GetEntitiesInTileFast().ToArray())
|
||||
{
|
||||
ReactWithEntity(entity, solutionFraction);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ReactWithEntity(IEntity entity, double solutionFraction);
|
||||
|
||||
public void TryAddSolution(Solution solution)
|
||||
{
|
||||
if (solution.TotalVolume == 0)
|
||||
return;
|
||||
|
||||
if (SolutionContainerComponent == null)
|
||||
return;
|
||||
|
||||
var addSolution =
|
||||
solution.SplitSolution(ReagentUnit.Min(solution.TotalVolume, SolutionContainerComponent.EmptyVolume));
|
||||
|
||||
SolutionContainerComponent.TryAddSolution(addSolution);
|
||||
|
||||
UpdateVisuals();
|
||||
}
|
||||
|
||||
protected abstract void UpdateVisuals();
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
Inception?.Remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <summary>
|
||||
/// The "mastermind" of a SolutionAreaEffect group. It gets updated by the SolutionAreaEffectSystem and tells the
|
||||
/// group when to spread, react and remove itself. This makes the group act like a single unit.
|
||||
/// </summary>
|
||||
/// <remarks> It should only be manually added to an entity by the <see cref="SolutionAreaEffectComponent"/> and not with a prototype.</remarks>
|
||||
[RegisterComponent]
|
||||
public class SolutionAreaEffectInceptionComponent : Component
|
||||
{
|
||||
public override string Name => "AreaEffectInception";
|
||||
|
||||
private const float ReactionDelay = 0.5f;
|
||||
|
||||
private readonly HashSet<SolutionAreaEffectComponent> _group = new();
|
||||
|
||||
[ViewVariables] private float _lifeTimer;
|
||||
[ViewVariables] private float _spreadTimer;
|
||||
[ViewVariables] private float _reactionTimer;
|
||||
|
||||
[ViewVariables] private int _amountCounterSpreading;
|
||||
[ViewVariables] private int _amountCounterRemoving;
|
||||
|
||||
/// <summary>
|
||||
/// How much time to wait after fully spread before starting to remove itself.
|
||||
/// </summary>
|
||||
[ViewVariables] private float _duration;
|
||||
|
||||
/// <summary>
|
||||
/// Time between each spread step. Decreasing this makes spreading faster.
|
||||
/// </summary>
|
||||
[ViewVariables] private float _spreadDelay;
|
||||
|
||||
/// <summary>
|
||||
/// Time between each remove step. Decreasing this makes removing faster.
|
||||
/// </summary>
|
||||
[ViewVariables] private float _removeDelay;
|
||||
|
||||
/// <summary>
|
||||
/// How many times will the effect react. As some entities from the group last a different amount of time than
|
||||
/// others, they will react a different amount of times, so we calculate the average to make the group behave
|
||||
/// a bit more uniformly.
|
||||
/// </summary>
|
||||
[ViewVariables] private float _averageExposures;
|
||||
|
||||
public void Setup(int amount, float duration, float spreadDelay, float removeDelay)
|
||||
{
|
||||
_amountCounterSpreading = amount;
|
||||
_duration = duration;
|
||||
_spreadDelay = spreadDelay;
|
||||
_removeDelay = removeDelay;
|
||||
|
||||
// So the first square reacts immediately after spawning
|
||||
_reactionTimer = ReactionDelay;
|
||||
/*
|
||||
The group takes amount*spreadDelay seconds to fully spread, same with fully disappearing.
|
||||
The outer squares will last duration seconds.
|
||||
The first square will last duration + how many seconds the group takes to fully spread and fully disappear, so
|
||||
it will last duration + amount*(spreadDelay+removeDelay).
|
||||
Thus, the average lifetime of the smokes will be (outerSmokeLifetime + firstSmokeLifetime)/2 = duration + amount*(spreadDelay+removeDelay)/2
|
||||
*/
|
||||
_averageExposures = (duration + amount * (spreadDelay+removeDelay) / 2)/ReactionDelay;
|
||||
}
|
||||
|
||||
public void InceptionUpdate(float frameTime)
|
||||
{
|
||||
_group.RemoveWhere(effect => effect.Deleted);
|
||||
if (_group.Count == 0)
|
||||
return;
|
||||
|
||||
// Make every outer square from the group spread
|
||||
if (_amountCounterSpreading > 0)
|
||||
{
|
||||
_spreadTimer += frameTime;
|
||||
if (_spreadTimer > _spreadDelay)
|
||||
{
|
||||
_spreadTimer -= _spreadDelay;
|
||||
|
||||
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterSpreading));
|
||||
foreach (var effect in outerEffects)
|
||||
{
|
||||
effect.Spread();
|
||||
}
|
||||
|
||||
_amountCounterSpreading -= 1;
|
||||
}
|
||||
}
|
||||
// Start counting for _duration after fully spreading
|
||||
else
|
||||
{
|
||||
_lifeTimer += frameTime;
|
||||
}
|
||||
|
||||
// Delete every outer square
|
||||
if (_lifeTimer > _duration)
|
||||
{
|
||||
_spreadTimer += frameTime;
|
||||
if (_spreadTimer > _removeDelay)
|
||||
{
|
||||
_spreadTimer -= _removeDelay;
|
||||
|
||||
var outerEffects = new HashSet<SolutionAreaEffectComponent>(_group.Where(effect => effect.Amount == _amountCounterRemoving));
|
||||
foreach (var effect in outerEffects)
|
||||
{
|
||||
effect.Kill();
|
||||
}
|
||||
|
||||
_amountCounterRemoving += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Make every square from the group react with the tile and entities
|
||||
_reactionTimer += frameTime;
|
||||
if (_reactionTimer > ReactionDelay)
|
||||
{
|
||||
_reactionTimer -= ReactionDelay;
|
||||
foreach (var effect in _group)
|
||||
{
|
||||
effect.React(_averageExposures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(SolutionAreaEffectComponent effect)
|
||||
{
|
||||
_group.Add(effect);
|
||||
effect.Inception = this;
|
||||
}
|
||||
|
||||
public void Remove(SolutionAreaEffectComponent effect)
|
||||
{
|
||||
_group.Remove(effect);
|
||||
effect.Inception = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedSolutionContainerComponent))]
|
||||
[ComponentReference(typeof(ISolutionInteractionsComponent))]
|
||||
public class SolutionContainerComponent : SharedSolutionContainerComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gives click behavior for transferring to/from other reagent containers.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class SolutionTransferComponent : Component, IAfterInteract
|
||||
{
|
||||
// Behavior is as such:
|
||||
// If it's a reagent tank, TAKE reagent.
|
||||
// If it's anything else, GIVE reagent.
|
||||
// Of course, only if possible.
|
||||
|
||||
public override string Name => "SolutionTransfer";
|
||||
|
||||
/// <summary>
|
||||
/// The amount of solution to be transferred from this solution when clicking on other solutions with it.
|
||||
/// </summary>
|
||||
[DataField("transferAmount")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(5);
|
||||
|
||||
/// <summary>
|
||||
/// Can this entity take reagent from reagent tanks?
|
||||
/// </summary>
|
||||
[DataField("canReceive")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool CanReceive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Can this entity give reagent to other reagent containers?
|
||||
/// </summary>
|
||||
[DataField("canSend")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool CanSend { get; set; } = true;
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.InRangeUnobstructed() || eventArgs.Target == null)
|
||||
return false;
|
||||
|
||||
if (!Owner.TryGetComponent(out ISolutionInteractionsComponent? ownerSolution))
|
||||
return false;
|
||||
|
||||
var target = eventArgs.Target;
|
||||
if (!target.TryGetComponent(out ISolutionInteractionsComponent? targetSolution))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CanReceive && target.TryGetComponent(out ReagentTankComponent? tank)
|
||||
&& ownerSolution.CanRefill && targetSolution.CanDrain)
|
||||
{
|
||||
var transferred = DoTransfer(targetSolution, ownerSolution, tank.TransferAmount, eventArgs.User);
|
||||
if (transferred > 0)
|
||||
{
|
||||
var toTheBrim = ownerSolution.RefillSpaceAvailable == 0;
|
||||
var msg = toTheBrim
|
||||
? "You fill {0:TheName} to the brim with {1}u from {2:theName}"
|
||||
: "You fill {0:TheName} with {1}u from {2:theName}";
|
||||
|
||||
target.PopupMessage(eventArgs.User, Loc.GetString(msg, Owner, transferred, target));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CanSend && targetSolution.CanRefill && ownerSolution.CanDrain)
|
||||
{
|
||||
var transferred = DoTransfer(ownerSolution, targetSolution, TransferAmount, eventArgs.User);
|
||||
|
||||
if (transferred > 0)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("You transfer {0}u to {1:theName}.",
|
||||
transferred, target));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <returns>The actual amount transferred.</returns>
|
||||
private static ReagentUnit DoTransfer(
|
||||
ISolutionInteractionsComponent source,
|
||||
ISolutionInteractionsComponent target,
|
||||
ReagentUnit amount,
|
||||
IEntity user)
|
||||
{
|
||||
if (source.DrainAvailable == 0)
|
||||
{
|
||||
source.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is empty!", source.Owner));
|
||||
return ReagentUnit.Zero;
|
||||
}
|
||||
|
||||
if (target.RefillSpaceAvailable == 0)
|
||||
{
|
||||
target.Owner.PopupMessage(user, Loc.GetString("{0:TheName} is full!", target.Owner));
|
||||
return ReagentUnit.Zero;
|
||||
}
|
||||
|
||||
var actualAmount =
|
||||
ReagentUnit.Min(amount, ReagentUnit.Min(source.DrainAvailable, target.RefillSpaceAvailable));
|
||||
|
||||
var solution = source.Drain(actualAmount);
|
||||
target.Refill(solution);
|
||||
|
||||
return actualAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class TransformableContainerComponent : Component, ISolutionChange
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override string Name => "TransformableContainer";
|
||||
|
||||
private SpriteSpecifier? _initialSprite;
|
||||
private string _initialName = default!;
|
||||
private string _initialDescription = default!;
|
||||
private ReagentPrototype? _currentReagent;
|
||||
|
||||
public bool Transformed { get; private set; }
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? sprite) &&
|
||||
sprite.BaseRSIPath != null)
|
||||
{
|
||||
_initialSprite = new SpriteSpecifier.Rsi(new ResourcePath(sprite.BaseRSIPath), "icon");
|
||||
}
|
||||
|
||||
_initialName = Owner.Name;
|
||||
_initialDescription = Owner.Description;
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
Owner.EnsureComponentWarn(out SolutionContainerComponent solution);
|
||||
|
||||
solution.Capabilities |= SolutionContainerCaps.FitsInDispenser;
|
||||
}
|
||||
|
||||
public void CancelTransformation()
|
||||
{
|
||||
_currentReagent = null;
|
||||
Transformed = false;
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? sprite) &&
|
||||
_initialSprite != null)
|
||||
{
|
||||
sprite.LayerSetSprite(0, _initialSprite);
|
||||
}
|
||||
|
||||
Owner.Name = _initialName;
|
||||
Owner.Description = _initialDescription;
|
||||
}
|
||||
|
||||
void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs)
|
||||
{
|
||||
var solution = eventArgs.Owner.GetComponent<SolutionContainerComponent>();
|
||||
//Transform container into initial state when emptied
|
||||
if (_currentReagent != null && solution.ReagentList.Count == 0)
|
||||
{
|
||||
CancelTransformation();
|
||||
}
|
||||
|
||||
//the biggest reagent in the solution decides the appearance
|
||||
var reagentId = solution.Solution.GetPrimaryReagentId();
|
||||
|
||||
//If biggest reagent didn't changed - don't change anything at all
|
||||
if (_currentReagent != null && _currentReagent.ID == reagentId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//Only reagents with spritePath property can change appearance of transformable containers!
|
||||
if (!string.IsNullOrWhiteSpace(reagentId) &&
|
||||
_prototypeManager.TryIndex(reagentId, out ReagentPrototype? proto) &&
|
||||
!string.IsNullOrWhiteSpace(proto.SpriteReplacementPath))
|
||||
{
|
||||
var spriteSpec = new SpriteSpecifier.Rsi(new ResourcePath("Objects/Consumable/Drinks/" + proto.SpriteReplacementPath),"icon");
|
||||
|
||||
if (Owner.TryGetComponent(out SpriteComponent? sprite))
|
||||
{
|
||||
sprite?.LayerSetSprite(0, spriteSpec);
|
||||
}
|
||||
|
||||
Owner.Name = proto.Name + " glass";
|
||||
Owner.Description = proto.Description;
|
||||
_currentReagent = proto;
|
||||
Transformed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Chemistry
|
||||
{
|
||||
[RegisterComponent]
|
||||
class VaporComponent : SharedVaporComponent, IStartCollide
|
||||
{
|
||||
public const float ReactTime = 0.125f;
|
||||
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("transferAmount")]
|
||||
private ReagentUnit _transferAmount = ReagentUnit.New(0.5);
|
||||
|
||||
private bool _reached;
|
||||
private float _reactTimer;
|
||||
private float _timer;
|
||||
private EntityCoordinates _target;
|
||||
private bool _running;
|
||||
private float _aliveTime;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Owner.EnsureComponentWarn(out SolutionContainerComponent _);
|
||||
}
|
||||
|
||||
public void Start(Vector2 dir, float speed, EntityCoordinates target, float aliveTime)
|
||||
{
|
||||
_running = true;
|
||||
_target = target;
|
||||
_aliveTime = aliveTime;
|
||||
// Set Move
|
||||
if (Owner.TryGetComponent(out PhysicsComponent? physics))
|
||||
{
|
||||
physics.BodyStatus = BodyStatus.InAir;
|
||||
physics.ApplyLinearImpulse(dir * speed);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
|
||||
return;
|
||||
|
||||
if (!_running)
|
||||
return;
|
||||
|
||||
_timer += frameTime;
|
||||
_reactTimer += frameTime;
|
||||
|
||||
if (_reactTimer >= ReactTime && Owner.Transform.GridID.IsValid())
|
||||
{
|
||||
_reactTimer = 0;
|
||||
var mapGrid = _mapManager.GetGrid(Owner.Transform.GridID);
|
||||
|
||||
var tile = mapGrid.GetTileRef(Owner.Transform.Coordinates.ToVector2i(Owner.EntityManager, _mapManager));
|
||||
foreach (var reagentQuantity in contents.ReagentList.ToArray())
|
||||
{
|
||||
if (reagentQuantity.Quantity == ReagentUnit.Zero) continue;
|
||||
var reagent = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.ReagentId);
|
||||
contents.TryRemoveReagent(reagentQuantity.ReagentId, reagent.ReactionTile(tile, (reagentQuantity.Quantity / _transferAmount) * 0.25f));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we've reached our target.
|
||||
if(!_reached && _target.TryDistance(Owner.EntityManager, Owner.Transform.Coordinates, out var distance) && distance <= 0.5f)
|
||||
{
|
||||
_reached = true;
|
||||
}
|
||||
|
||||
if (contents.CurrentVolume == 0 || _timer > _aliveTime)
|
||||
{
|
||||
// Delete this
|
||||
Owner.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryAddSolution(Solution solution)
|
||||
{
|
||||
if (solution.TotalVolume == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = contents.TryAddSolution(solution);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? contents))
|
||||
return;
|
||||
|
||||
contents.Solution.DoEntityReaction(otherFixture.Body.Owner, ReactionMethod.Touch);
|
||||
|
||||
// Check for collision with a impassable object (e.g. wall) and stop
|
||||
if ((otherFixture.CollisionLayer & (int) CollisionGroup.Impassable) != 0 && otherFixture.Hard)
|
||||
{
|
||||
Owner.QueueDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Content.Shared.Chemistry;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CleanableComponent : Component
|
||||
{
|
||||
public override string Name => "Cleanable";
|
||||
|
||||
[DataField("cleanAmount")]
|
||||
private ReagentUnit _cleanAmount = ReagentUnit.Zero;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public ReagentUnit CleanAmount => _cleanAmount;
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using Content.Server.GameObjects.Components.PDA;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Interfaces.Chat;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components.Command;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Command
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
public class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent, IActivate
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
|
||||
|
||||
private RoundEndSystem RoundEndSystem => EntitySystem.Get<RoundEndSystem>();
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CommunicationsConsoleUiKey.Key);
|
||||
|
||||
public TimeSpan LastAnnounceTime { get; private set; } = TimeSpan.Zero;
|
||||
public TimeSpan AnnounceCooldown { get; } = TimeSpan.FromSeconds(90);
|
||||
private CancellationTokenSource _announceCooldownEndedTokenSource = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
|
||||
}
|
||||
|
||||
RoundEndSystem.OnRoundEndCountdownStarted += UpdateBoundInterface;
|
||||
RoundEndSystem.OnRoundEndCountdownCancelled += UpdateBoundInterface;
|
||||
RoundEndSystem.OnRoundEndCountdownFinished += UpdateBoundInterface;
|
||||
RoundEndSystem.OnCallCooldownEnded += UpdateBoundInterface;
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
UpdateBoundInterface();
|
||||
}
|
||||
|
||||
private void UpdateBoundInterface()
|
||||
{
|
||||
if (!Deleted)
|
||||
{
|
||||
var system = RoundEndSystem;
|
||||
|
||||
UserInterface?.SetState(new CommunicationsConsoleInterfaceState(CanAnnounce(), system.CanCall(), system.ExpectedCountdownEnd));
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanAnnounce()
|
||||
{
|
||||
if (LastAnnounceTime == TimeSpan.Zero)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return _gameTiming.CurTime >= LastAnnounceTime + AnnounceCooldown;
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
RoundEndSystem.OnRoundEndCountdownStarted -= UpdateBoundInterface;
|
||||
RoundEndSystem.OnRoundEndCountdownCancelled -= UpdateBoundInterface;
|
||||
RoundEndSystem.OnRoundEndCountdownFinished -= UpdateBoundInterface;
|
||||
base.OnRemove();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
|
||||
{
|
||||
switch (obj.Message)
|
||||
{
|
||||
case CommunicationsConsoleCallEmergencyShuttleMessage _:
|
||||
RoundEndSystem.RequestRoundEnd();
|
||||
break;
|
||||
|
||||
case CommunicationsConsoleRecallEmergencyShuttleMessage _:
|
||||
RoundEndSystem.CancelRoundEndCountdown();
|
||||
break;
|
||||
case CommunicationsConsoleAnnounceMessage msg:
|
||||
if (!CanAnnounce())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_announceCooldownEndedTokenSource.Cancel();
|
||||
_announceCooldownEndedTokenSource = new CancellationTokenSource();
|
||||
LastAnnounceTime = _gameTiming.CurTime;
|
||||
Timer.Spawn(AnnounceCooldown, () => UpdateBoundInterface(), _announceCooldownEndedTokenSource.Token);
|
||||
UpdateBoundInterface();
|
||||
|
||||
var message = msg.Message.Length <= 256 ? msg.Message.Trim() : $"{msg.Message.Trim().Substring(0, 256)}...";
|
||||
|
||||
var author = "Unknown";
|
||||
var mob = obj.Session.AttachedEntity;
|
||||
if (mob != null && mob.TryGetHeldId(out var id))
|
||||
{
|
||||
author = $"{id.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id.JobTitle ?? string.Empty)})".Trim();
|
||||
}
|
||||
|
||||
message += $"\nSent by {author}";
|
||||
_chatManager.DispatchStationAnnouncement(message, "Communications Console");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenUserInterface(IPlayerSession session)
|
||||
{
|
||||
UserInterface?.Open(session);
|
||||
}
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
return;
|
||||
/*
|
||||
if (!Powered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
*/
|
||||
OpenUserInterface(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
using Content.Server.GameObjects.Components.Construction;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ComputerComponent : SharedComputerComponent, IMapInit
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("board")]
|
||||
private string? _boardPrototype;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// Let's ensure the container manager and container are here.
|
||||
Owner.EnsureContainer<Container>("board", out var _);
|
||||
|
||||
if (Owner.TryGetComponent(out PowerReceiverComponent? powerReceiver) &&
|
||||
Owner.TryGetComponent(out AppearanceComponent? appearance))
|
||||
{
|
||||
appearance.SetData(ComputerVisuals.Powered, powerReceiver.Powered);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
PowerReceiverOnOnPowerStateChanged(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void PowerReceiverOnOnPowerStateChanged(PowerChangedMessage e)
|
||||
{
|
||||
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
|
||||
{
|
||||
appearance.SetData(ComputerVisuals.Powered, e.Powered);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the corresponding computer board on the computer.
|
||||
/// This exists so when you deconstruct computers that were serialized with the map,
|
||||
/// you can retrieve the computer board.
|
||||
/// </summary>
|
||||
private void CreateComputerBoard()
|
||||
{
|
||||
// Ensure that the construction component is aware of the board container.
|
||||
if (Owner.TryGetComponent(out ConstructionComponent? construction))
|
||||
construction.AddContainer("board");
|
||||
|
||||
// We don't do anything if this is null or empty.
|
||||
if (string.IsNullOrEmpty(_boardPrototype))
|
||||
return;
|
||||
|
||||
var container = Owner.EnsureContainer<Container>("board", out var existed);
|
||||
|
||||
if (existed)
|
||||
{
|
||||
// We already contain a board. Note: We don't check if it's the right one!
|
||||
if (container.ContainedEntities.Count != 0)
|
||||
return;
|
||||
}
|
||||
|
||||
var board = Owner.EntityManager.SpawnEntity(_boardPrototype, Owner.Transform.Coordinates);
|
||||
|
||||
if(!container.Insert(board))
|
||||
Logger.Warning($"Couldn't insert board {board} to computer {Owner}!");
|
||||
}
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
CreateComputerBoard();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Components.Interactable;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedConfigurationComponent))]
|
||||
public class ConfigurationComponent : SharedConfigurationComponent, IInteractUsing, ISerializationHooks
|
||||
{
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ConfigurationUiKey.Key);
|
||||
|
||||
[DataField("keys")] private List<string> _keys = new();
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<string, string> _config = new();
|
||||
|
||||
[DataField("validation")]
|
||||
private readonly Regex _validation = new ("^[a-zA-Z0-9 ]*$", RegexOptions.Compiled);
|
||||
|
||||
void ISerializationHooks.BeforeSerialization()
|
||||
{
|
||||
_keys = _config.Keys.ToList();
|
||||
}
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
foreach (var key in _keys)
|
||||
{
|
||||
_config.Add(key, "");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnAdd()
|
||||
{
|
||||
base.OnAdd();
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage -= UserInterfaceOnReceiveMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetConfig(string name)
|
||||
{
|
||||
return _config.GetValueOrDefault(name);
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (UserInterface == null || !eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
return false;
|
||||
|
||||
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
|
||||
return false;
|
||||
|
||||
if (!await tool.UseTool(eventArgs.User, Owner, 0.2f, ToolQuality.Multitool))
|
||||
return false;
|
||||
|
||||
OpenUserInterface(actor);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
|
||||
{
|
||||
var message = serverMsg.Message;
|
||||
var config = new Dictionary<string, string>(_config);
|
||||
|
||||
if (message is ConfigurationUpdatedMessage msg)
|
||||
{
|
||||
foreach (var key in config.Keys)
|
||||
{
|
||||
var value = msg.Config.GetValueOrDefault(key);
|
||||
|
||||
if (value == null || _validation != null && !_validation.IsMatch(value) && value != "")
|
||||
continue;
|
||||
|
||||
_config[key] = value;
|
||||
}
|
||||
|
||||
SendMessage(new ConfigUpdatedComponentMessage(config));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUserInterface()
|
||||
{
|
||||
UserInterface?.SetState(new ConfigurationBoundUserInterfaceState(_config));
|
||||
}
|
||||
|
||||
private void OpenUserInterface(ActorComponent actor)
|
||||
{
|
||||
UpdateUserInterface();
|
||||
UserInterface?.Open(actor.PlayerSession);
|
||||
UserInterface?.SendMessage(new ValidationUpdateMessage(_validation.ToString()), actor.PlayerSession);
|
||||
}
|
||||
|
||||
private static void FillConfiguration<T>(List<string> list, Dictionary<string, T> configuration, T value){
|
||||
for (var index = 0; index < list.Count; index++)
|
||||
{
|
||||
configuration.Add(list[index], value);
|
||||
}
|
||||
}
|
||||
|
||||
[Verb]
|
||||
public sealed class ConfigureVerb : Verb<ConfigurationComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ConfigurationComponent component, VerbData data)
|
||||
{
|
||||
var session = user.PlayerSession();
|
||||
var groupController = IoCManager.Resolve<IConGroupController>();
|
||||
if (session == null || !groupController.CanAdminMenu(session))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Open Configuration");
|
||||
data.IconTexture = "/Textures/Interface/VerbIcons/settings.svg.192dpi.png";
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ConfigurationComponent component)
|
||||
{
|
||||
if (user.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
component.OpenUserInterface(actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ComputerBoardComponent : Component
|
||||
{
|
||||
public override string Name => "ComputerBoard";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("prototype")]
|
||||
public string? Prototype { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
public partial class ConstructionComponent
|
||||
{
|
||||
[Verb]
|
||||
public sealed class DeconstructibleVerb : Verb<ConstructionComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, ConstructionComponent component, VerbData data)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
if (((component.Target != null) && (component.Target.Name == component.DeconstructionNodeIdentifier)) ||
|
||||
((component.Node != null) && (component.Node.Name == component.DeconstructionNodeIdentifier)))
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.CategoryData = VerbCategories.Construction;
|
||||
data.Text = Loc.GetString("Begin deconstructing");
|
||||
data.IconTexture = "/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png";
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, ConstructionComponent component)
|
||||
{
|
||||
component.SetNewTarget(component.DeconstructionNodeIdentifier);
|
||||
if (component.Target == null)
|
||||
{
|
||||
// Maybe check, but on the flip-side a better solution might be to not make it undeconstructible in the first place, no?
|
||||
component.Owner.PopupMessage(user, Loc.GetString("There is no way to deconstruct this."));
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Owner.PopupMessage(user, Loc.GetString("Examine to see instructions."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Server.GameObjects.Components.Stack;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.GameObjects.Components.Interactable;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
[RegisterComponent]
|
||||
public partial class ConstructionComponent : Component, IExamine, IInteractUsing
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override string Name => "Construction";
|
||||
|
||||
private bool _handling = false;
|
||||
|
||||
private TaskCompletionSource<object>? _handlingTask = null;
|
||||
[DataField("graph")]
|
||||
private string _graphIdentifier = string.Empty;
|
||||
[DataField("node")]
|
||||
private string _startingNodeIdentifier = string.Empty;
|
||||
[DataField("defaultTarget")]
|
||||
private string _startingTargetNodeIdentifier = string.Empty;
|
||||
|
||||
[ViewVariables]
|
||||
private HashSet<string> _containers = new();
|
||||
[ViewVariables]
|
||||
private List<List<ConstructionGraphStep>>? _edgeNestedStepProgress = null;
|
||||
|
||||
private ConstructionGraphNode? _target = null;
|
||||
|
||||
[ViewVariables]
|
||||
public ConstructionGraphPrototype? GraphPrototype { get; private set; }
|
||||
|
||||
[ViewVariables]
|
||||
public ConstructionGraphNode? Node { get; private set; } = null;
|
||||
|
||||
[ViewVariables]
|
||||
public ConstructionGraphEdge? Edge { get; private set; } = null;
|
||||
|
||||
public IReadOnlyCollection<string> Containers => _containers;
|
||||
|
||||
[ViewVariables]
|
||||
int IInteractUsing.Priority => 2;
|
||||
|
||||
[ViewVariables]
|
||||
public ConstructionGraphNode? Target
|
||||
{
|
||||
get => _target;
|
||||
set
|
||||
{
|
||||
ClearTarget();
|
||||
_target = value;
|
||||
UpdateTarget();
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
public ConstructionGraphEdge? TargetNextEdge { get; private set; } = null;
|
||||
|
||||
[ViewVariables]
|
||||
public Queue<ConstructionGraphNode>? TargetPathfinding { get; private set; } = null;
|
||||
|
||||
[ViewVariables]
|
||||
public int EdgeStep { get; private set; } = 0;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("deconstructionTarget")]
|
||||
public string DeconstructionNodeIdentifier { get; private set; } = "start";
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to set a new pathfinding target.
|
||||
/// </summary>
|
||||
public void SetNewTarget(string node)
|
||||
{
|
||||
if (GraphPrototype != null && GraphPrototype.Nodes.TryGetValue(node, out var target))
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearTarget()
|
||||
{
|
||||
_target = null;
|
||||
TargetNextEdge = null;
|
||||
TargetPathfinding = null;
|
||||
}
|
||||
|
||||
public void UpdateTarget()
|
||||
{
|
||||
// Can't pathfind without a target or no node.
|
||||
if (Target == null || Node == null || GraphPrototype == null) return;
|
||||
|
||||
// If we're at our target, stop pathfinding.
|
||||
if (Target == Node)
|
||||
{
|
||||
ClearTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we don't have the path, set it!
|
||||
if (TargetPathfinding == null)
|
||||
{
|
||||
var path = GraphPrototype.Path(Node.Name, Target.Name);
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
ClearTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
TargetPathfinding = new Queue<ConstructionGraphNode>(path);
|
||||
}
|
||||
|
||||
// Dequeue the pathfinding queue if the next is the node we're at.
|
||||
if (TargetPathfinding.Peek() == Node)
|
||||
TargetPathfinding.Dequeue();
|
||||
|
||||
// If we went the wrong way, we stop pathfinding.
|
||||
if (Edge != null && TargetNextEdge != Edge)
|
||||
{
|
||||
ClearTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's set the next target edge.
|
||||
if (Edge == null && TargetNextEdge == null && TargetPathfinding != null)
|
||||
TargetNextEdge = Node.GetEdge(TargetPathfinding.Peek().Name);
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (_handling)
|
||||
return true;
|
||||
|
||||
_handlingTask = new TaskCompletionSource<object>();
|
||||
_handling = true;
|
||||
bool result;
|
||||
|
||||
if (Edge == null)
|
||||
result = await HandleNode(eventArgs);
|
||||
else
|
||||
result = await HandleEdge(eventArgs);
|
||||
|
||||
_handling = false;
|
||||
_handlingTask.SetResult(null!);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<bool> HandleNode(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
EdgeStep = 0;
|
||||
|
||||
if (Node == null || GraphPrototype == null) return false;
|
||||
|
||||
foreach (var edge in Node.Edges)
|
||||
{
|
||||
if(edge.Steps.Count == 0)
|
||||
throw new InvalidDataException($"Edge to \"{edge.Target}\" in node \"{Node.Name}\" of graph \"{GraphPrototype.ID}\" doesn't have any steps!");
|
||||
|
||||
var firstStep = edge.Steps[0];
|
||||
switch (firstStep)
|
||||
{
|
||||
case MaterialConstructionGraphStep _:
|
||||
case ToolConstructionGraphStep _:
|
||||
case ArbitraryInsertConstructionGraphStep _:
|
||||
if (await HandleStep(eventArgs, edge, firstStep))
|
||||
{
|
||||
if(edge.Steps.Count > 1)
|
||||
Edge = edge;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case NestedConstructionGraphStep nestedStep:
|
||||
throw new IndexOutOfRangeException($"Nested construction step not supported as the first step in an edge! Graph: {GraphPrototype.ID} Node: {Node.Name} Edge: {edge.Target}");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> HandleStep(InteractUsingEventArgs eventArgs, ConstructionGraphEdge? edge = null, ConstructionGraphStep? step = null, bool nested = false)
|
||||
{
|
||||
edge ??= Edge;
|
||||
step ??= edge?.Steps[EdgeStep];
|
||||
|
||||
if (edge == null || step == null)
|
||||
return false;
|
||||
|
||||
foreach (var condition in edge.Conditions)
|
||||
{
|
||||
if (!await condition.Condition(Owner)) return false;
|
||||
}
|
||||
|
||||
var handled = false;
|
||||
|
||||
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
||||
|
||||
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, step.DoAfter, default, eventArgs.Target)
|
||||
{
|
||||
BreakOnDamage = false,
|
||||
BreakOnStun = true,
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
NeedHand = true,
|
||||
};
|
||||
|
||||
switch (step)
|
||||
{
|
||||
case ToolConstructionGraphStep toolStep:
|
||||
// Gotta take welder fuel into consideration.
|
||||
if (toolStep.Tool == ToolQuality.Welding)
|
||||
{
|
||||
if (eventArgs.Using.TryGetComponent(out WelderComponent? welder) &&
|
||||
await welder.UseTool(eventArgs.User, Owner, step.DoAfter, toolStep.Tool, toolStep.Fuel))
|
||||
{
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (eventArgs.Using.TryGetComponent(out ToolComponent? tool) &&
|
||||
await tool.UseTool(eventArgs.User, Owner, step.DoAfter, toolStep.Tool))
|
||||
{
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
// To prevent too much code duplication.
|
||||
case EntityInsertConstructionGraphStep insertStep:
|
||||
var valid = false;
|
||||
var entityUsing = eventArgs.Using;
|
||||
|
||||
switch (insertStep)
|
||||
{
|
||||
case ArbitraryInsertConstructionGraphStep arbitraryStep:
|
||||
if (arbitraryStep.EntityValid(eventArgs.Using)
|
||||
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
|
||||
{
|
||||
valid = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case MaterialConstructionGraphStep materialStep:
|
||||
if (materialStep.EntityValid(eventArgs.Using, out var stack)
|
||||
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
|
||||
{
|
||||
var splitStack = new StackSplitEvent() {Amount = materialStep.Amount, SpawnPosition = eventArgs.User.Transform.Coordinates};
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(stack.Owner.Uid, splitStack);
|
||||
|
||||
if (splitStack.Result != null)
|
||||
{
|
||||
entityUsing = splitStack.Result;
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!valid || entityUsing == null) break;
|
||||
|
||||
if(string.IsNullOrEmpty(insertStep.Store))
|
||||
{
|
||||
entityUsing.Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
_containers.Add(insertStep.Store);
|
||||
var container = Owner.EnsureContainer<Container>(insertStep.Store);
|
||||
container.Insert(entityUsing);
|
||||
}
|
||||
|
||||
handled = true;
|
||||
|
||||
break;
|
||||
|
||||
case NestedConstructionGraphStep nestedStep:
|
||||
if(_edgeNestedStepProgress == null)
|
||||
_edgeNestedStepProgress = new List<List<ConstructionGraphStep>>(nestedStep.Steps);
|
||||
|
||||
foreach (var list in _edgeNestedStepProgress.ToArray())
|
||||
{
|
||||
if (list.Count == 0)
|
||||
{
|
||||
_edgeNestedStepProgress.Remove(list);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!await HandleStep(eventArgs, edge, list[0], true)) continue;
|
||||
|
||||
list.RemoveAt(0);
|
||||
|
||||
// We check again...
|
||||
if (list.Count == 0)
|
||||
_edgeNestedStepProgress.Remove(list);
|
||||
}
|
||||
|
||||
if (_edgeNestedStepProgress.Count == 0)
|
||||
handled = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled)
|
||||
{
|
||||
foreach (var completed in step.Completed)
|
||||
{
|
||||
await completed.PerformAction(Owner, eventArgs.User);
|
||||
|
||||
if (Owner.Deleted)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (nested && handled) return true;
|
||||
if (!handled) return false;
|
||||
|
||||
EdgeStep++;
|
||||
|
||||
if (edge.Steps.Count == EdgeStep)
|
||||
{
|
||||
await HandleCompletion(edge, eventArgs.User);
|
||||
}
|
||||
|
||||
UpdateTarget();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> HandleCompletion(ConstructionGraphEdge edge, IEntity user)
|
||||
{
|
||||
if (edge.Steps.Count != EdgeStep || GraphPrototype == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Edge = edge;
|
||||
|
||||
UpdateTarget();
|
||||
|
||||
TargetNextEdge = null;
|
||||
Edge = null;
|
||||
Node = GraphPrototype.Nodes[edge.Target];
|
||||
|
||||
foreach (var completed in edge.Completed)
|
||||
{
|
||||
await completed.PerformAction(Owner, user);
|
||||
if (Owner.Deleted) return true;
|
||||
}
|
||||
|
||||
// Perform node actions!
|
||||
foreach (var action in Node.Actions)
|
||||
{
|
||||
await action.PerformAction(Owner, user);
|
||||
|
||||
if (Owner.Deleted)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Target == Node)
|
||||
ClearTarget();
|
||||
|
||||
await HandleEntityChange(Node, user);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetEdge()
|
||||
{
|
||||
_edgeNestedStepProgress = null;
|
||||
TargetNextEdge = null;
|
||||
Edge = null;
|
||||
EdgeStep = 0;
|
||||
|
||||
UpdateTarget();
|
||||
}
|
||||
|
||||
private async Task<bool> HandleEdge(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (Edge == null || EdgeStep >= Edge.Steps.Count) return false;
|
||||
|
||||
return await HandleStep(eventArgs, Edge, Edge.Steps[EdgeStep]);
|
||||
}
|
||||
|
||||
private async Task<bool> HandleEntityChange(ConstructionGraphNode node, IEntity? user = null)
|
||||
{
|
||||
if (node.Entity == Owner.Prototype?.ID || string.IsNullOrEmpty(node.Entity)
|
||||
|| GraphPrototype == null) return false;
|
||||
|
||||
var entity = Owner.EntityManager.SpawnEntity(node.Entity, Owner.Transform.Coordinates);
|
||||
|
||||
entity.Transform.LocalRotation = Owner.Transform.LocalRotation;
|
||||
|
||||
if (entity.TryGetComponent(out ConstructionComponent? construction))
|
||||
{
|
||||
if(construction.GraphPrototype != GraphPrototype)
|
||||
throw new Exception($"New entity {node.Entity}'s graph {construction.GraphPrototype?.ID ?? null} isn't the same as our graph {GraphPrototype.ID} on node {node.Name}!");
|
||||
|
||||
construction.Node = node;
|
||||
construction.Target = Target;
|
||||
construction._containers = new HashSet<string>(_containers);
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out ContainerManagerComponent? containerComp))
|
||||
{
|
||||
foreach (var container in _containers)
|
||||
{
|
||||
var otherContainer = entity.EnsureContainer<Container>(container);
|
||||
var ourContainer = containerComp.GetContainer(container);
|
||||
|
||||
foreach (var ent in ourContainer.ContainedEntities.ToArray())
|
||||
{
|
||||
ourContainer.ForceRemove(ent);
|
||||
otherContainer.Insert(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out IPhysBody? physics) &&
|
||||
entity.TryGetComponent(out IPhysBody? otherPhysics))
|
||||
{
|
||||
otherPhysics.BodyType = physics.BodyType;
|
||||
}
|
||||
|
||||
Owner.Delete();
|
||||
|
||||
foreach (var action in node.Actions)
|
||||
{
|
||||
await action.PerformAction(entity, user);
|
||||
|
||||
if (entity.Deleted)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AddContainer(string id)
|
||||
{
|
||||
return _containers.Add(id);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
if (string.IsNullOrEmpty(_graphIdentifier))
|
||||
{
|
||||
Logger.Warning($"Prototype {Owner.Prototype?.ID}'s construction component didn't have a graph identifier!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_prototypeManager.TryIndex(_graphIdentifier, out ConstructionGraphPrototype? graph))
|
||||
{
|
||||
GraphPrototype = graph;
|
||||
|
||||
if (GraphPrototype.Nodes.TryGetValue(_startingNodeIdentifier, out var node))
|
||||
{
|
||||
Node = node;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error($"Couldn't find node {_startingNodeIdentifier} in graph {_graphIdentifier} in construction component!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error($"Couldn't find prototype {_graphIdentifier} in construction component!");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_startingTargetNodeIdentifier))
|
||||
SetNewTarget(_startingTargetNodeIdentifier);
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
if (Node == null) return;
|
||||
|
||||
foreach (var action in Node.Actions)
|
||||
{
|
||||
action.PerformAction(Owner, null);
|
||||
|
||||
if (Owner.Deleted)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ChangeNode(string node)
|
||||
{
|
||||
if (GraphPrototype == null) return;
|
||||
|
||||
var graphNode = GraphPrototype.Nodes[node];
|
||||
|
||||
if (_handling && _handlingTask?.Task != null)
|
||||
await _handlingTask.Task;
|
||||
|
||||
Edge = null;
|
||||
Node = graphNode;
|
||||
|
||||
foreach (var action in Node.Actions)
|
||||
{
|
||||
await action.PerformAction(Owner, null);
|
||||
if (Owner.Deleted)
|
||||
return;
|
||||
}
|
||||
|
||||
await HandleEntityChange(graphNode);
|
||||
}
|
||||
|
||||
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
|
||||
{
|
||||
if(Target != null)
|
||||
message.AddMarkup(Loc.GetString("To create {0}...\n", Target.Name));
|
||||
|
||||
if (Edge == null && TargetNextEdge != null)
|
||||
{
|
||||
var preventStepExamine = false;
|
||||
|
||||
foreach (var condition in TargetNextEdge.Conditions)
|
||||
{
|
||||
preventStepExamine |= condition.DoExamine(Owner, message, inDetailsRange);
|
||||
}
|
||||
|
||||
if(!preventStepExamine)
|
||||
TargetNextEdge.Steps[0].DoExamine(message, inDetailsRange);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Edge != null)
|
||||
{
|
||||
var preventStepExamine = false;
|
||||
|
||||
foreach (var condition in Edge.Conditions)
|
||||
{
|
||||
preventStepExamine |= condition.DoExamine(Owner, message, inDetailsRange);
|
||||
}
|
||||
|
||||
if (preventStepExamine) return;
|
||||
}
|
||||
|
||||
if (_edgeNestedStepProgress == null)
|
||||
{
|
||||
if(EdgeStep < Edge?.Steps.Count)
|
||||
Edge.Steps[EdgeStep].DoExamine(message, inDetailsRange);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var list in _edgeNestedStepProgress)
|
||||
{
|
||||
if(list.Count == 0) continue;
|
||||
|
||||
list[0].DoExamine(message, inDetailsRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Construction;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Stacks;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class MachineBoardComponent : Component, IExamine
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
public override string Name => "MachineBoard";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("requirements")]
|
||||
public readonly Dictionary<MachinePart, int> Requirements = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("materialRequirements")]
|
||||
public readonly Dictionary<string, int> MaterialIdRequirements = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("tagRequirements")]
|
||||
public readonly Dictionary<string, GenericPartInfo> TagRequirements = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("componentRequirements")]
|
||||
public readonly Dictionary<string, GenericPartInfo> ComponentRequirements = new();
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("prototype")]
|
||||
public string? Prototype { get; private set; }
|
||||
|
||||
public IEnumerable<KeyValuePair<StackPrototype, int>> MaterialRequirements
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var (materialId, amount) in MaterialIdRequirements)
|
||||
{
|
||||
var material = _prototypeManager.Index<StackPrototype>(materialId);
|
||||
yield return new KeyValuePair<StackPrototype, int>(material, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Examine(FormattedMessage message, bool inDetailsRange)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("Requires:\n"));
|
||||
foreach (var (part, amount) in Requirements)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", amount, Loc.GetString(part.ToString())));
|
||||
}
|
||||
|
||||
foreach (var (material, amount) in MaterialRequirements)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", amount, Loc.GetString(material.Name)));
|
||||
}
|
||||
|
||||
foreach (var (_, info) in ComponentRequirements)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", info.Amount, Loc.GetString(info.ExamineName)));
|
||||
}
|
||||
|
||||
foreach (var (_, info) in TagRequirements)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("[color=yellow]{0}x[/color] [color=green]{1}[/color]\n", info.Amount, Loc.GetString(info.ExamineName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
[DataDefinition]
|
||||
public struct GenericPartInfo
|
||||
{
|
||||
[DataField("Amount")]
|
||||
public int Amount;
|
||||
[DataField("ExamineName")]
|
||||
public string ExamineName;
|
||||
[DataField("DefaultPrototype")]
|
||||
public string DefaultPrototype;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class MachineComponent : Component, IMapInit
|
||||
{
|
||||
public override string Name => "Machine";
|
||||
|
||||
[DataField("board")]
|
||||
public string? BoardPrototype { get; private set; }
|
||||
|
||||
private Container _boardContainer = default!;
|
||||
private Container _partContainer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_boardContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.BoardContainer);
|
||||
_partContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.PartContainer);
|
||||
}
|
||||
|
||||
public IEnumerable<MachinePartComponent> GetAllParts()
|
||||
{
|
||||
foreach (var entity in _partContainer.ContainedEntities)
|
||||
{
|
||||
if (entity.TryGetComponent<MachinePartComponent>(out var machinePart))
|
||||
yield return machinePart;
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshParts()
|
||||
{
|
||||
foreach (var refreshable in Owner.GetAllComponents<IRefreshParts>())
|
||||
{
|
||||
refreshable.RefreshParts(GetAllParts());
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateBoardAndStockParts()
|
||||
{
|
||||
// Entity might not be initialized yet.
|
||||
var boardContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.BoardContainer, out var existedBoard);
|
||||
var partContainer = Owner.EnsureContainer<Container>(MachineFrameComponent.PartContainer, out var existedParts);
|
||||
|
||||
if (string.IsNullOrEmpty(BoardPrototype))
|
||||
return;
|
||||
|
||||
var entityManager = Owner.EntityManager;
|
||||
|
||||
if (existedBoard || existedParts)
|
||||
{
|
||||
// We're done here, let's suppose all containers are correct just so we don't screw SaveLoadSave.
|
||||
if (boardContainer.ContainedEntities.Count > 0)
|
||||
return;
|
||||
}
|
||||
|
||||
var board = entityManager.SpawnEntity(BoardPrototype, Owner.Transform.Coordinates);
|
||||
|
||||
if (!_boardContainer.Insert(board))
|
||||
{
|
||||
throw new Exception($"Couldn't insert board with prototype {BoardPrototype} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}!");
|
||||
}
|
||||
|
||||
if (!board.TryGetComponent<MachineBoardComponent>(out var machineBoard))
|
||||
{
|
||||
throw new Exception($"Entity with prototype {BoardPrototype} doesn't have a {nameof(MachineBoardComponent)}!");
|
||||
}
|
||||
|
||||
foreach (var (part, amount) in machineBoard.Requirements)
|
||||
{
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
var p = entityManager.SpawnEntity(MachinePartComponent.Prototypes[part], Owner.Transform.Coordinates);
|
||||
|
||||
if (!partContainer.Insert(p))
|
||||
throw new Exception($"Couldn't insert machine part of type {part} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}!");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (stackType, amount) in machineBoard.MaterialRequirements)
|
||||
{
|
||||
var stackSpawn = new StackTypeSpawnEvent()
|
||||
{Amount = amount, StackType = stackType, SpawnPosition = Owner.Transform.Coordinates};
|
||||
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, stackSpawn);
|
||||
|
||||
var s = stackSpawn.Result;
|
||||
|
||||
if (s == null)
|
||||
throw new Exception($"Couldn't spawn stack of type {stackType}!");
|
||||
|
||||
if (!partContainer.Insert(s))
|
||||
throw new Exception($"Couldn't insert machine material of type {stackType} to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
|
||||
}
|
||||
|
||||
foreach (var (compName, info) in machineBoard.ComponentRequirements)
|
||||
{
|
||||
for (var i = 0; i < info.Amount; i++)
|
||||
{
|
||||
var c = entityManager.SpawnEntity(info.DefaultPrototype, Owner.Transform.Coordinates);
|
||||
|
||||
if(!partContainer.Insert(c))
|
||||
throw new Exception($"Couldn't insert machine component part with default prototype '{compName}' to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (tagName, info) in machineBoard.TagRequirements)
|
||||
{
|
||||
for (var i = 0; i < info.Amount; i++)
|
||||
{
|
||||
var c = entityManager.SpawnEntity(info.DefaultPrototype, Owner.Transform.Coordinates);
|
||||
|
||||
if(!partContainer.Insert(c))
|
||||
throw new Exception($"Couldn't insert machine component part with default prototype '{tagName}' to machine with prototype {Owner.Prototype?.ID ?? "N/A"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void MapInit()
|
||||
{
|
||||
CreateBoardAndStockParts();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.GameObjects.Components.Stack;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Components.Construction;
|
||||
using Content.Shared.GameObjects.Components.Tag;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class MachineFrameComponent : Component, IInteractUsing
|
||||
{
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
|
||||
public const string PartContainer = "machine_parts";
|
||||
public const string BoardContainer = "machine_board";
|
||||
|
||||
public override string Name => "MachineFrame";
|
||||
|
||||
[ViewVariables]
|
||||
public bool IsComplete
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasBoard || Requirements == null || MaterialRequirements == null)
|
||||
return false;
|
||||
|
||||
foreach (var (part, amount) in Requirements)
|
||||
{
|
||||
if (_progress[part] < amount)
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var (type, amount) in MaterialRequirements)
|
||||
{
|
||||
if (_materialProgress[type] < amount)
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var (compName, info) in ComponentRequirements)
|
||||
{
|
||||
if (_componentProgress[compName] < info.Amount)
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var (tagName, info) in TagRequirements)
|
||||
{
|
||||
if (_tagProgress[tagName] < info.Amount)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
public bool HasBoard => _boardContainer?.ContainedEntities.Count != 0;
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<MachinePart, int> _progress = new();
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<string, int> _materialProgress = new();
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<string, int> _componentProgress = new();
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<string, int> _tagProgress = new();
|
||||
|
||||
[ViewVariables]
|
||||
private Dictionary<MachinePart, int> _requirements = new();
|
||||
|
||||
[ViewVariables]
|
||||
private Dictionary<string, int> _materialRequirements = new();
|
||||
|
||||
[ViewVariables]
|
||||
private Dictionary<string, GenericPartInfo> _componentRequirements = new();
|
||||
|
||||
[ViewVariables]
|
||||
private Dictionary<string, GenericPartInfo> _tagRequirements = new();
|
||||
|
||||
[ViewVariables]
|
||||
private Container _boardContainer = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private Container _partContainer = default!;
|
||||
|
||||
public IReadOnlyDictionary<MachinePart, int> Progress => _progress;
|
||||
|
||||
public IReadOnlyDictionary<string, int> MaterialProgress => _materialProgress;
|
||||
|
||||
public IReadOnlyDictionary<string, int> ComponentProgress => _componentProgress;
|
||||
|
||||
public IReadOnlyDictionary<string, int> TagProgress => _tagProgress;
|
||||
|
||||
public IReadOnlyDictionary<MachinePart, int> Requirements => _requirements;
|
||||
|
||||
public IReadOnlyDictionary<string, int> MaterialRequirements => _materialRequirements;
|
||||
|
||||
public IReadOnlyDictionary<string, GenericPartInfo> ComponentRequirements => _componentRequirements;
|
||||
|
||||
public IReadOnlyDictionary<string, GenericPartInfo> TagRequirements => _tagRequirements;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_boardContainer = ContainerHelpers.EnsureContainer<Container>(Owner, BoardContainer);
|
||||
_partContainer = ContainerHelpers.EnsureContainer<Container>(Owner, PartContainer);
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
RegenerateProgress();
|
||||
|
||||
if (Owner.TryGetComponent<ConstructionComponent>(out var construction))
|
||||
{
|
||||
// Attempt to set pathfinding to the machine node...
|
||||
construction.SetNewTarget("machine");
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetProgressAndRequirements(MachineBoardComponent machineBoard)
|
||||
{
|
||||
_requirements = machineBoard.Requirements;
|
||||
_materialRequirements = machineBoard.MaterialIdRequirements;
|
||||
_componentRequirements = machineBoard.ComponentRequirements;
|
||||
_tagRequirements = machineBoard.TagRequirements;
|
||||
|
||||
_progress.Clear();
|
||||
_materialProgress.Clear();
|
||||
_componentProgress.Clear();
|
||||
_tagProgress.Clear();
|
||||
|
||||
foreach (var (machinePart, _) in Requirements)
|
||||
{
|
||||
_progress[machinePart] = 0;
|
||||
}
|
||||
|
||||
foreach (var (stackType, _) in MaterialRequirements)
|
||||
{
|
||||
_materialProgress[stackType] = 0;
|
||||
}
|
||||
|
||||
foreach (var (compName, _) in ComponentRequirements)
|
||||
{
|
||||
_componentProgress[compName] = 0;
|
||||
}
|
||||
|
||||
foreach (var (compName, _) in TagRequirements)
|
||||
{
|
||||
_tagProgress[compName] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegenerateProgress()
|
||||
{
|
||||
AppearanceComponent? appearance;
|
||||
|
||||
if (!HasBoard)
|
||||
{
|
||||
if (Owner.TryGetComponent(out appearance))
|
||||
{
|
||||
appearance.SetData(MachineFrameVisuals.State, 1);
|
||||
}
|
||||
|
||||
_requirements.Clear();
|
||||
_materialRequirements.Clear();
|
||||
_componentRequirements.Clear();
|
||||
_tagRequirements.Clear();
|
||||
_progress.Clear();
|
||||
_materialProgress.Clear();
|
||||
_componentProgress.Clear();
|
||||
_tagProgress.Clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var board = _boardContainer.ContainedEntities[0];
|
||||
|
||||
if (!board.TryGetComponent<MachineBoardComponent>(out var machineBoard))
|
||||
return;
|
||||
|
||||
if (Owner.TryGetComponent(out appearance))
|
||||
{
|
||||
appearance.SetData(MachineFrameVisuals.State, 2);
|
||||
}
|
||||
|
||||
ResetProgressAndRequirements(machineBoard);
|
||||
|
||||
foreach (var part in _partContainer.ContainedEntities)
|
||||
{
|
||||
if (part.TryGetComponent<MachinePartComponent>(out var machinePart))
|
||||
{
|
||||
// Check this is part of the requirements...
|
||||
if (!Requirements.ContainsKey(machinePart.PartType))
|
||||
continue;
|
||||
|
||||
if (!_progress.ContainsKey(machinePart.PartType))
|
||||
_progress[machinePart.PartType] = 1;
|
||||
else
|
||||
_progress[machinePart.PartType]++;
|
||||
}
|
||||
|
||||
if (part.TryGetComponent<StackComponent>(out var stack))
|
||||
{
|
||||
var type = stack.StackTypeId;
|
||||
// Check this is part of the requirements...
|
||||
if (!MaterialRequirements.ContainsKey(type))
|
||||
continue;
|
||||
|
||||
if (!_materialProgress.ContainsKey(type))
|
||||
_materialProgress[type] = 1;
|
||||
else
|
||||
_materialProgress[type]++;
|
||||
}
|
||||
|
||||
// I have many regrets.
|
||||
foreach (var (compName, _) in ComponentRequirements)
|
||||
{
|
||||
var registration = _componentFactory.GetRegistration(compName);
|
||||
|
||||
if (!part.HasComponent(registration.Type))
|
||||
continue;
|
||||
|
||||
if (!_componentProgress.ContainsKey(compName))
|
||||
_componentProgress[compName] = 1;
|
||||
else
|
||||
_componentProgress[compName]++;
|
||||
}
|
||||
|
||||
// I have MANY regrets.
|
||||
foreach (var (tagName, _) in TagRequirements)
|
||||
{
|
||||
if (!part.HasTag(tagName))
|
||||
continue;
|
||||
|
||||
if (!_tagProgress.ContainsKey(tagName))
|
||||
_tagProgress[tagName] = 1;
|
||||
else
|
||||
_tagProgress[tagName]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!HasBoard && eventArgs.Using.TryGetComponent<MachineBoardComponent>(out var machineBoard))
|
||||
{
|
||||
if (eventArgs.Using.TryRemoveFromContainer())
|
||||
{
|
||||
// Valid board!
|
||||
_boardContainer.Insert(eventArgs.Using);
|
||||
|
||||
// Setup requirements and progress...
|
||||
ResetProgressAndRequirements(machineBoard);
|
||||
|
||||
if (Owner.TryGetComponent<AppearanceComponent>(out var appearance))
|
||||
{
|
||||
appearance.SetData(MachineFrameVisuals.State, 2);
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out ConstructionComponent? construction))
|
||||
{
|
||||
// So prying the components off works correctly.
|
||||
construction.ResetEdge();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (HasBoard)
|
||||
{
|
||||
if (eventArgs.Using.TryGetComponent<MachinePartComponent>(out var machinePart))
|
||||
{
|
||||
if (!Requirements.ContainsKey(machinePart.PartType))
|
||||
return false;
|
||||
|
||||
if (_progress[machinePart.PartType] != Requirements[machinePart.PartType]
|
||||
&& eventArgs.Using.TryRemoveFromContainer() && _partContainer.Insert(eventArgs.Using))
|
||||
{
|
||||
_progress[machinePart.PartType]++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (eventArgs.Using.TryGetComponent<StackComponent>(out var stack))
|
||||
{
|
||||
var type = stack.StackTypeId;
|
||||
if (!MaterialRequirements.ContainsKey(type))
|
||||
return false;
|
||||
|
||||
if (_materialProgress[type] == MaterialRequirements[type])
|
||||
return false;
|
||||
|
||||
var needed = MaterialRequirements[type] - _materialProgress[type];
|
||||
var count = stack.Count;
|
||||
|
||||
if (count < needed)
|
||||
{
|
||||
if(!_partContainer.Insert(stack.Owner))
|
||||
return false;
|
||||
|
||||
_materialProgress[type] += count;
|
||||
return true;
|
||||
}
|
||||
|
||||
var splitStack = new StackSplitEvent()
|
||||
{Amount = needed, SpawnPosition = Owner.Transform.Coordinates};
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(stack.Owner.Uid, splitStack);
|
||||
|
||||
if (splitStack.Result == null)
|
||||
return false;
|
||||
|
||||
if(!_partContainer.Insert(splitStack.Result))
|
||||
return false;
|
||||
|
||||
_materialProgress[type] += needed;
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var (compName, info) in ComponentRequirements)
|
||||
{
|
||||
if (_componentProgress[compName] >= info.Amount)
|
||||
continue;
|
||||
|
||||
var registration = _componentFactory.GetRegistration(compName);
|
||||
|
||||
if (!eventArgs.Using.HasComponent(registration.Type))
|
||||
continue;
|
||||
|
||||
if (!eventArgs.Using.TryRemoveFromContainer() || !_partContainer.Insert(eventArgs.Using)) continue;
|
||||
_componentProgress[compName]++;
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var (tagName, info) in TagRequirements)
|
||||
{
|
||||
if (_tagProgress[tagName] >= info.Amount)
|
||||
continue;
|
||||
|
||||
if (!eventArgs.Using.HasTag(tagName))
|
||||
continue;
|
||||
|
||||
if (!eventArgs.Using.TryRemoveFromContainer() || !_partContainer.Insert(eventArgs.Using)) continue;
|
||||
_tagProgress[tagName]++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Construction;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class MachinePartComponent : Component, IExamine
|
||||
{
|
||||
// I'm so sorry for hard-coding this. But trust me, it should make things less painful.
|
||||
public static IReadOnlyDictionary<MachinePart, string> Prototypes { get; } = new Dictionary<MachinePart, string>()
|
||||
{
|
||||
{MachinePart.Capacitor, "CapacitorStockPart"},
|
||||
{MachinePart.ScanningModule, "ScanningModuleStockPart"},
|
||||
{MachinePart.Manipulator, "MicroManipulatorStockPart"},
|
||||
{MachinePart.Laser, "MicroLaserStockPart"},
|
||||
{MachinePart.MatterBin, "MatterBinStockPart"},
|
||||
{MachinePart.Ansible, "AnsibleSubspaceStockPart"},
|
||||
{MachinePart.Filter, "FilterSubspaceStockPart"},
|
||||
{MachinePart.Amplifier, "AmplifierSubspaceStockPart"},
|
||||
{MachinePart.Treatment, "TreatmentSubspaceStockPart"},
|
||||
{MachinePart.Analyzer, "AnalyzerSubspaceStockPart"},
|
||||
{MachinePart.Crystal, "CrystalSubspaceStockPart"},
|
||||
{MachinePart.Transmitter, "TransmitterSubspaceStockPart"}
|
||||
};
|
||||
|
||||
public override string Name => "MachinePart";
|
||||
|
||||
[ViewVariables] [DataField("part")] public MachinePart PartType { get; private set; } = MachinePart.Capacitor;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("rating")]
|
||||
public int Rating { get; private set; } = 1;
|
||||
|
||||
public void Examine(FormattedMessage message, bool inDetailsRange)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("[color=white]Rating:[/color] [color=cyan]{0}[/color]\n", Rating));
|
||||
message.AddMarkup(Loc.GetString("[color=white]Type:[/color] [color=cyan]{0}[/color]\n", PartType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Interactable;
|
||||
using Content.Server.GameObjects.Components.Stack;
|
||||
using Content.Shared.GameObjects.Components.Interactable;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Construction
|
||||
{
|
||||
/// <summary>
|
||||
/// Used for something that can be refined by welder.
|
||||
/// For example, glass shard can be refined to glass sheet.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class WelderRefinableComponent : Component, IInteractUsing
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("refineResult")]
|
||||
private HashSet<string>? _refineResult = new() { };
|
||||
[ViewVariables]
|
||||
[DataField("refineTime")]
|
||||
private float _refineTime = 2f;
|
||||
|
||||
private bool _beingWelded;
|
||||
|
||||
public override string Name => "WelderRefinable";
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
// check if object is welder
|
||||
if (!eventArgs.Using.TryGetComponent(out ToolComponent? tool))
|
||||
return false;
|
||||
|
||||
// check if someone is already welding object
|
||||
if (_beingWelded)
|
||||
return false;
|
||||
_beingWelded = true;
|
||||
|
||||
if (!await tool.UseTool(eventArgs.User, Owner, _refineTime, ToolQuality.Welding))
|
||||
{
|
||||
// failed to veld - abort refine
|
||||
_beingWelded = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// get last owner coordinates and delete it
|
||||
var resultPosition = Owner.Transform.Coordinates;
|
||||
Owner.Delete();
|
||||
|
||||
// spawn each result afrer refine
|
||||
foreach (var result in _refineResult!)
|
||||
{
|
||||
var droppedEnt = Owner.EntityManager.SpawnEntity(result, resultPosition);
|
||||
|
||||
// TODO: If something has a stack... Just use a prototype with a single thing in the stack.
|
||||
// This is not a good way to do it.
|
||||
if (droppedEnt.HasComponent<StackComponent>())
|
||||
Owner.EntityManager.EventBus.RaiseLocalEvent(droppedEnt.Uid, new StackChangeCountEvent(1), false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.GameObjects
|
||||
{
|
||||
public static class ContainerExt
|
||||
{
|
||||
public static int CountPrototypeOccurencesRecursive(this ContainerManagerComponent mgr, string prototypeId)
|
||||
{
|
||||
int total = 0;
|
||||
foreach (var container in mgr.GetAllContainers())
|
||||
{
|
||||
foreach (var entity in container.ContainedEntities)
|
||||
{
|
||||
if (entity.Prototype?.ID == prototypeId) total++;
|
||||
if(!entity.TryGetComponent<ContainerManagerComponent>(out var component)) continue;
|
||||
total += component.CountPrototypeOccurencesRecursive(prototypeId);
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.MachineLinking;
|
||||
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
|
||||
using Content.Shared.GameObjects.Components.Conveyor;
|
||||
using Content.Shared.GameObjects.Components.MachineLinking;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Conveyor
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ConveyorComponent : Component, ISignalReceiver<TwoWayLeverSignal>, ISignalReceiver<bool>
|
||||
{
|
||||
public override string Name => "Conveyor";
|
||||
|
||||
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
|
||||
|
||||
/// <summary>
|
||||
/// The angle to move entities by in relation to the owner's rotation.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("angle")]
|
||||
private Angle _angle;
|
||||
|
||||
public float Speed => _speed;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of units to move the entity by per second.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("speed")]
|
||||
private float _speed = 2f;
|
||||
|
||||
private ConveyorState _state;
|
||||
/// <summary>
|
||||
/// The current state of this conveyor
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
private ConveyorState State
|
||||
{
|
||||
get => _state;
|
||||
set
|
||||
{
|
||||
_state = value;
|
||||
UpdateAppearance();
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PowerChangedMessage powerChanged:
|
||||
OnPowerChanged(powerChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPowerChanged(PowerChangedMessage e)
|
||||
{
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (Owner.TryGetComponent<AppearanceComponent>(out var appearance))
|
||||
{
|
||||
if (Powered)
|
||||
{
|
||||
appearance.SetData(ConveyorVisuals.State, _state);
|
||||
}
|
||||
else
|
||||
{
|
||||
appearance.SetData(ConveyorVisuals.State, ConveyorState.Off);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the angle in which entities on top of this conveyor
|
||||
/// belt are pushed in
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The angle when taking into account if the conveyor is reversed
|
||||
/// </returns>
|
||||
public Angle GetAngle()
|
||||
{
|
||||
var adjustment = _state == ConveyorState.Reversed ? MathHelper.Pi : 0;
|
||||
var radians = MathHelper.DegreesToRadians(_angle);
|
||||
|
||||
return new Angle(Owner.Transform.LocalRotation.Theta + radians + adjustment);
|
||||
}
|
||||
|
||||
public bool CanRun()
|
||||
{
|
||||
if (State == ConveyorState.Off)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
|
||||
!receiver.Powered)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Owner.HasComponent<ItemComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CanMove(IEntity entity)
|
||||
{
|
||||
// TODO We should only check status InAir or Static or MapGrid or /mayber/ container
|
||||
if (entity == Owner)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!entity.TryGetComponent(out IPhysBody? physics) ||
|
||||
physics.BodyType == BodyType.Static)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entity.HasComponent<ConveyorComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entity.HasComponent<IMapGridComponent>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entity.IsInContainer())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void TriggerSignal(TwoWayLeverSignal signal)
|
||||
{
|
||||
State = signal switch
|
||||
{
|
||||
TwoWayLeverSignal.Left => ConveyorState.Reversed,
|
||||
TwoWayLeverSignal.Middle => ConveyorState.Off,
|
||||
TwoWayLeverSignal.Right => ConveyorState.Forward,
|
||||
_ => ConveyorState.Off
|
||||
};
|
||||
}
|
||||
|
||||
public void TriggerSignal(bool signal)
|
||||
{
|
||||
State = signal ? ConveyorState.Forward : ConveyorState.Off;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CrayonComponent : SharedCrayonComponent, IAfterInteract, IUse, IDropped, ISerializationHooks
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
//TODO: useSound
|
||||
[DataField("useSound")]
|
||||
private string? _useSound = string.Empty;
|
||||
|
||||
[ViewVariables]
|
||||
public Color Color { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Charges { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("capacity")]
|
||||
public int Capacity { get; set; } = 30;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CrayonUiKey.Key);
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
Color = Color.FromName(_color);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
|
||||
}
|
||||
Charges = Capacity;
|
||||
|
||||
// Get the first one from the catalog and set it as default
|
||||
var decals = _prototypeManager.EnumeratePrototypes<CrayonDecalPrototype>().FirstOrDefault();
|
||||
if (decals != null)
|
||||
{
|
||||
SelectedState = decals.Decals.First();
|
||||
}
|
||||
Dirty();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
|
||||
{
|
||||
switch (serverMsg.Message)
|
||||
{
|
||||
case CrayonSelectMessage msg:
|
||||
// Check if the selected state is valid
|
||||
var crayonDecals = _prototypeManager.EnumeratePrototypes<CrayonDecalPrototype>().FirstOrDefault();
|
||||
if (crayonDecals != null)
|
||||
{
|
||||
if (crayonDecals.Decals.Contains(msg.State))
|
||||
{
|
||||
SelectedState = msg.State;
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState(ICommonSession player)
|
||||
{
|
||||
return new CrayonComponentState(_color, SelectedState, Charges, Capacity);
|
||||
}
|
||||
|
||||
// Opens the selection window
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
{
|
||||
UserInterface?.Toggle(actor.PlayerSession);
|
||||
if (UserInterface?.SessionHasOpen(actor.PlayerSession) == true)
|
||||
{
|
||||
// Tell the user interface the selected stuff
|
||||
UserInterface.SetState(
|
||||
new CrayonBoundUserInterfaceState(SelectedState, Color));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: false, popup: true,
|
||||
collisionMask: Shared.Physics.CollisionGroup.MobImpassable))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Charges <= 0)
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("Not enough left."));
|
||||
return true;
|
||||
}
|
||||
|
||||
var entityManager = IoCManager.Resolve<IServerEntityManager>();
|
||||
|
||||
var entity = entityManager.SpawnEntity("CrayonDecal", eventArgs.ClickLocation);
|
||||
if (entity.TryGetComponent(out AppearanceComponent? appearance))
|
||||
{
|
||||
appearance.SetData(CrayonVisuals.State, SelectedState);
|
||||
appearance.SetData(CrayonVisuals.Color, _color);
|
||||
appearance.SetData(CrayonVisuals.Rotation, eventArgs.User.Transform.LocalRotation);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_useSound))
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(Owner), _useSound, Owner, AudioHelpers.WithVariation(0.125f));
|
||||
}
|
||||
|
||||
// Decrease "Ammo"
|
||||
Charges--;
|
||||
Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
void IDropped.Dropped(DroppedEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.User.TryGetComponent(out ActorComponent? actor))
|
||||
UserInterface?.Close(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Chemistry;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Nutrition;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Culinary
|
||||
{
|
||||
[RegisterComponent]
|
||||
class SliceableFoodComponent : Component, IInteractUsing, IExamine
|
||||
{
|
||||
public override string Name => "SliceableFood";
|
||||
|
||||
int IInteractUsing.Priority => 1; // take priority over eating with utensils
|
||||
|
||||
[DataField("slice")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
private string _slice = string.Empty;
|
||||
|
||||
[DataField("sound")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
private string _sound = "/Audio/Items/Culinary/chop.ogg";
|
||||
|
||||
[DataField("count")] [ViewVariables(VVAccess.ReadWrite)]
|
||||
private ushort _totalCount = 5;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)] public ushort Count;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
Count = _totalCount;
|
||||
Owner.EnsureComponent<FoodComponent>();
|
||||
Owner.EnsureComponent<SolutionContainerComponent>();
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_slice))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Owner.TryGetComponent(out SolutionContainerComponent? solution))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!eventArgs.Using.TryGetComponent(out UtensilComponent? utensil) || !utensil.HasType(UtensilType.Knife))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var itemToSpawn = Owner.EntityManager.SpawnEntity(_slice, Owner.Transform.Coordinates);
|
||||
if (eventArgs.User.TryGetComponent(out HandsComponent? handsComponent))
|
||||
{
|
||||
if (ContainerHelpers.IsInContainer(Owner))
|
||||
{
|
||||
handsComponent.PutInHandOrDrop(itemToSpawn.GetComponent<ItemComponent>());
|
||||
}
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(Owner), _sound, Owner.Transform.Coordinates,
|
||||
AudioParams.Default.WithVolume(-2));
|
||||
|
||||
Count--;
|
||||
if (Count < 1)
|
||||
{
|
||||
Owner.Delete();
|
||||
return true;
|
||||
}
|
||||
solution.TryRemoveReagent("Nutriment", solution.CurrentVolume / ReagentUnit.New(Count + 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Examine(FormattedMessage message, bool inDetailsRange)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString($"There are { Count } slices remaining."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Nutrition;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Culinary
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class UtensilComponent : Component, IAfterInteract
|
||||
{
|
||||
public override string Name => "Utensil";
|
||||
|
||||
[DataField("types")]
|
||||
private UtensilType _types = UtensilType.None;
|
||||
|
||||
[ViewVariables]
|
||||
public UtensilType Types
|
||||
{
|
||||
get => _types;
|
||||
set
|
||||
{
|
||||
if (_types.Equals(value))
|
||||
return;
|
||||
|
||||
_types = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The chance that the utensil has to break with each use.
|
||||
/// A value of 0 means that it is unbreakable.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("breakChance")]
|
||||
private float _breakChance;
|
||||
|
||||
/// <summary>
|
||||
/// The sound to be played if the utensil breaks.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("breakSound")]
|
||||
private string? _breakSound = "/Audio/Items/snap.ogg";
|
||||
|
||||
public void AddType(UtensilType type)
|
||||
{
|
||||
Types |= type;
|
||||
}
|
||||
|
||||
public bool HasAnyType(UtensilType type)
|
||||
{
|
||||
return (_types & type) != UtensilType.None;
|
||||
}
|
||||
|
||||
public bool HasType(UtensilType type)
|
||||
{
|
||||
return (_types & type) != 0;
|
||||
}
|
||||
|
||||
public void RemoveType(UtensilType type)
|
||||
{
|
||||
Types &= ~type;
|
||||
}
|
||||
|
||||
internal void TryBreak(IEntity user)
|
||||
{
|
||||
if (_breakSound != null && IoCManager.Resolve<IRobustRandom>().Prob(_breakChance))
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(user), _breakSound, user, AudioParams.Default.WithVolume(-2f));
|
||||
Owner.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
return TryUseUtensil(eventArgs.User, eventArgs.Target);
|
||||
}
|
||||
|
||||
private bool TryUseUtensil(IEntity user, IEntity? target)
|
||||
{
|
||||
if (target == null || !target.TryGetComponent(out FoodComponent? food))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.InRangeUnobstructed(target, popup: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
food.TryUseFood(user, null, this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum UtensilType : byte
|
||||
{
|
||||
None = 0,
|
||||
Fork = 1,
|
||||
Spoon = 1 << 1,
|
||||
Knife = 1 << 2
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user