Re-organize all projects (#4166)
This commit is contained in:
442
Content.Server/Chemistry/Components/ChemMasterComponent.cs
Normal file
442
Content.Server/Chemistry/Components/ChemMasterComponent.cs
Normal file
@@ -0,0 +1,442 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Items;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Solution;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Notification;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.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.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Content.Server.Body.Circulatory;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Physics.Collision;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ChemicalInjectionProjectileComponent : Component, IStartCollide
|
||||
{
|
||||
public override string Name => "ChemicalInjectionProjectile";
|
||||
|
||||
[ViewVariables]
|
||||
private SolutionContainerComponent _solutionContainer = default!;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("transferAmount")]
|
||||
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(1);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
|
||||
[DataField("transferEfficiency")]
|
||||
private float _transferEfficiency = 1f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_solutionContainer = Owner.EnsureComponent<SolutionContainerComponent>();
|
||||
}
|
||||
|
||||
void IStartCollide.CollideWith(Fixture ourFixture, Fixture otherFixture, in Manifold manifold)
|
||||
{
|
||||
if (!otherFixture.Body.Owner.TryGetComponent<BloodstreamComponent>(out var bloodstream))
|
||||
return;
|
||||
|
||||
var solution = _solutionContainer.Solution;
|
||||
var solRemoved = solution.SplitSolution(TransferAmount);
|
||||
var solRemovedVol = solRemoved.TotalVolume;
|
||||
|
||||
var solToInject = solRemoved.SplitSolution(solRemovedVol * TransferEfficiency);
|
||||
|
||||
bloodstream.TryTransferSolution(solToInject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#nullable enable
|
||||
using Content.Server.Body.Circulatory;
|
||||
using Content.Server.Inventory.Components;
|
||||
using Content.Server.Items;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Foam;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
119
Content.Server/Chemistry/Components/HyposprayComponent.cs
Normal file
119
Content.Server/Chemistry/Components/HyposprayComponent.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using Content.Server.Interaction.Components;
|
||||
using Content.Server.MobState.States;
|
||||
using Content.Server.Weapon.Melee;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Notification;
|
||||
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.Chemistry.Components
|
||||
{
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
302
Content.Server/Chemistry/Components/InjectorComponent.cs
Normal file
302
Content.Server/Chemistry/Components/InjectorComponent.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Body.Circulatory;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Solution.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Notification;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class MeleeChemicalInjectorComponent : Component
|
||||
{
|
||||
public override string Name => "MeleeChemicalInjector";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("transferAmount")]
|
||||
public ReagentUnit TransferAmount { get; set; } = ReagentUnit.New(1);
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TransferEfficiency { get => _transferEfficiency; set => _transferEfficiency = Math.Clamp(value, 0, 1); }
|
||||
|
||||
[DataField("transferEfficiency")]
|
||||
private float _transferEfficiency = 1f;
|
||||
}
|
||||
}
|
||||
112
Content.Server/Chemistry/Components/PillComponent.cs
Normal file
112
Content.Server/Chemistry/Components/PillComponent.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Body.Behavior;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Notification;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
391
Content.Server/Chemistry/Components/ReagentDispenserComponent.cs
Normal file
391
Content.Server/Chemistry/Components/ReagentDispenserComponent.cs
Normal file
@@ -0,0 +1,391 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Items;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Dispenser;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Solution;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Notification;
|
||||
using Content.Shared.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.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Content.Server/Chemistry/Components/ReagentTankComponent.cs
Normal file
29
Content.Server/Chemistry/Components/ReagentTankComponent.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[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
|
||||
}
|
||||
}
|
||||
57
Content.Server/Chemistry/Components/RehydratableComponent.cs
Normal file
57
Content.Server/Chemistry/Components/RehydratableComponent.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
#nullable enable
|
||||
using Content.Server.Notification;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.Body.Circulatory;
|
||||
using Content.Server.Body.Respiratory;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Smoking;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.Coordinates.Helpers;
|
||||
using Content.Server.GameObjects.Components.Atmos;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Solution;
|
||||
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.Chemistry.Components
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
using Content.Shared.Chemistry.Solution.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedSolutionContainerComponent))]
|
||||
[ComponentReference(typeof(ISolutionInteractionsComponent))]
|
||||
public class SolutionContainerComponent : SharedSolutionContainerComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
123
Content.Server/Chemistry/Components/SolutionTransferComponent.cs
Normal file
123
Content.Server/Chemistry/Components/SolutionTransferComponent.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Solution.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Notification;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#nullable enable
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Solution;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Chemistry.Components
|
||||
{
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
Content.Server/Chemistry/Components/VaporComponent.cs
Normal file
131
Content.Server/Chemistry/Components/VaporComponent.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Solution;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Vapor;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
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.Chemistry.Components
|
||||
{
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user