Анализатор растений (#1755)
Co-authored-by: Ignaz Kraft <ignaz.k@live.de>
This commit is contained in:
parent
b4d0729d3b
commit
2c1db3ffdf
29 changed files with 1445 additions and 256 deletions
|
|
@ -0,0 +1,42 @@
|
|||
using Content.Shared.Botany.PlantAnalyzer;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Botany.PlantAnalyzer;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class PlantAnalyzerBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[ViewVariables]
|
||||
private PlantAnalyzerWindow? _window;
|
||||
|
||||
public PlantAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = this.CreateWindow<PlantAnalyzerWindow>();
|
||||
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
_window.Print.OnPressed += _ => Print();
|
||||
}
|
||||
|
||||
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (message is not PlantAnalyzerScannedUserMessage cast)
|
||||
return;
|
||||
|
||||
_window.Populate(cast);
|
||||
}
|
||||
|
||||
private void Print()
|
||||
{
|
||||
SendMessage(new PlantAnalyzerPrintMessage());
|
||||
if (_window != null)
|
||||
_window.Print.Disabled = true;
|
||||
}
|
||||
}
|
||||
121
Content.Client/Botany/PlantAnalyzer/PlantAnalyzerWindow.xaml
Normal file
121
Content.Client/Botany/PlantAnalyzer/PlantAnalyzerWindow.xaml
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinWidth="320"
|
||||
MaxWidth="320">
|
||||
<ScrollContainer
|
||||
Margin="5 5 5 5"
|
||||
ReturnMeasure="True"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="False">
|
||||
<BoxContainer
|
||||
Name="RootContainer"
|
||||
VerticalExpand="True"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer
|
||||
Name="DataContainer"
|
||||
Margin="0 0 0 5"
|
||||
Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 5">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64" />
|
||||
<TextureRect Name="NoDataIcon" Access="Public" SetSize="64 64" Visible="false" Stretch="KeepAspectCentered" TexturePath="/Textures/Interface/Misc/health_analyzer_out_of_range.png"/>
|
||||
<BoxContainer Margin="5 0 0 0" Orientation="Vertical" VerticalAlignment="Top">
|
||||
<RichTextLabel Name="SeedLabel" SetWidth="150"
|
||||
Text="{Loc 'generic-unknown'}"/>
|
||||
<Label Name="ContainerLabel" VerticalAlignment="Top" StyleClasses="LabelSubText"
|
||||
Text="{Loc 'generic-unknown'}"/>
|
||||
</BoxContainer>
|
||||
<Label Margin="0 0 5 0" HorizontalExpand="True" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
VerticalAlignment="Top" Name="ScanModeLabel"
|
||||
Text="{Loc 'health-analyzer-window-entity-unknown-text'}" />
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Name="PlantDataTags" Margin="5 5 0 0" Orientation="Horizontal" Visible="false">
|
||||
<RichTextLabel Margin="0 0 8 0" Name="Alive" Visible="false" Text="{Loc 'plant-analyzer-component-alive'}" />
|
||||
<RichTextLabel Margin="0 0 8 0" Name="Dead" Visible="false" Text="{Loc 'plant-analyzer-component-dead'}" />
|
||||
<RichTextLabel Margin="0 0 8 0" Name="Unviable" Visible="false" Text="{Loc 'plant-analyzer-component-unviable'}" />
|
||||
<RichTextLabel Margin="0 0 8 0" Name="Kudzu" Visible="false" Text="{Loc 'plant-analyzer-component-kudzu'}" />
|
||||
<RichTextLabel Margin="0 0 8 0" Name="Mutating" Visible="false" Text="{Loc 'plant-analyzer-component-mutating'}" />
|
||||
</BoxContainer>
|
||||
<GridContainer Name="PlantDataGrid" Margin="0 0 0 5" Columns="6" Visible="false">
|
||||
<Label Text=" · " />
|
||||
<Label Text="{Loc 'plant-analyzer-component-health'}" />
|
||||
<Label SetWidth="8" />
|
||||
<Label HorizontalAlignment="Right" Name="Health" />
|
||||
<Label Text=" / " />
|
||||
<Label HorizontalAlignment="Right" Name="Endurance" />
|
||||
<Label Text=" · " />
|
||||
<Label Text="{Loc 'plant-analyzer-component-age'}" />
|
||||
<Label SetWidth="8" />
|
||||
<Label HorizontalAlignment="Right" Name="Age" />
|
||||
<Label Text=" / " />
|
||||
<Label HorizontalAlignment="Right" Name="Lifespan" />
|
||||
</GridContainer>
|
||||
<PanelContainer Name="PlantDataDivider" Visible="false" StyleClasses="LowDivider" />
|
||||
<GridContainer Name="ContainerGrid" Margin="0 5" Columns="8" Visible="false">
|
||||
<!-- Max values from `PlantHolderSystem.CheckLevelSanity` -->
|
||||
<Label Text=" · " />
|
||||
<Label Text="{Loc 'plant-analyzer-component-water'}" />
|
||||
<Label SetWidth="8" />
|
||||
<Label FontColorOverride="cyan" HorizontalAlignment="Right" Name="WaterLevelLabel" />
|
||||
<Label Text=" / " />
|
||||
<Label HorizontalAlignment="Right" Text="100.00" />
|
||||
<Label Margin="12 0 0 0" Name="GtFieldIfTolerances1" />
|
||||
<Label HorizontalAlignment="Right" Name="WaterConsumptionLabel" />
|
||||
<Label Text=" · " />
|
||||
<Label Text="{Loc 'plant-analyzer-component-nutrition'}" />
|
||||
<Label SetWidth="8" />
|
||||
<Label FontColorOverride="orange" HorizontalAlignment="Right" Name="NutritionLevelLabel" />
|
||||
<Label Text=" / " />
|
||||
<Label HorizontalAlignment="Right" Text="100.00" />
|
||||
<Label Margin="12 0 0 0" Name="GtFieldIfTolerances2" />
|
||||
<Label HorizontalAlignment="Right" Name="NutritionConsumptionLabel" />
|
||||
<Label Text=" · " />
|
||||
<Label Text="{Loc 'plant-analyzer-component-toxins'}" />
|
||||
<Label SetWidth="8" />
|
||||
<Label FontColorOverride="yellowgreen" HorizontalAlignment="Right" Name="ToxinsLabel" />
|
||||
<Label Text=" / " />
|
||||
<Label HorizontalAlignment="Right" Text="100.00" />
|
||||
<Label Margin="12 0 0 0" Name="LtFieldIfTolerances1" />
|
||||
<Label HorizontalAlignment="Right" Name="ToxinsResistanceLabel" />
|
||||
<Label Text=" · " />
|
||||
<Label Text="{Loc 'plant-analyzer-component-pests'}" />
|
||||
<Label SetWidth="8" />
|
||||
<Label FontColorOverride="magenta" HorizontalAlignment="Right" Name="PestLevelLabel" />
|
||||
<Label Text=" / " />
|
||||
<Label HorizontalAlignment="Right" Text="10.00" />
|
||||
<Label Margin="12 0 0 0" Name="LtFieldIfTolerances2" />
|
||||
<Label HorizontalAlignment="Right" Name="PestResistanceLabel" />
|
||||
<Label Text=" · " />
|
||||
<Label Text="{Loc 'plant-analyzer-component-weeds'}" />
|
||||
<Label SetWidth="8" />
|
||||
<Label FontColorOverride="red" HorizontalAlignment="Right" Name="WeedLevelLabel" />
|
||||
<Label Text=" / " />
|
||||
<Label HorizontalAlignment="Right" Text="10.00" />
|
||||
<Label Margin="12 0 0 0" Name="LtFieldIfTolerances3" />
|
||||
<Label HorizontalAlignment="Right" Name="WeedResistanceLabel" />
|
||||
</GridContainer>
|
||||
<PanelContainer Name="ContainerDivider" Visible="false" StyleClasses="LowDivider" />
|
||||
<BoxContainer Name="ChemicalsInWaterBox" Visible="false" Orientation="Horizontal" Margin="5">
|
||||
<RichTextLabel Name="ChemicalsInWaterLabel" SetWidth="290" />
|
||||
</BoxContainer>
|
||||
<PanelContainer Name="ChemicalsInWaterDivider" Visible="false" StyleClasses="LowDivider" />
|
||||
<BoxContainer Name="EnvironmentBox" Visible="false" Orientation="Horizontal" Margin="5">
|
||||
<RichTextLabel Name="EnvironmentLabel" SetWidth="290" />
|
||||
</BoxContainer>
|
||||
<PanelContainer Name="EnvironmentDivider" Visible="false" StyleClasses="LowDivider" />
|
||||
<BoxContainer Name="ProduceBox" Visible="false" Orientation="Horizontal" Margin="5">
|
||||
<RichTextLabel Name="ProduceLabel" SetWidth="290" />
|
||||
</BoxContainer>
|
||||
<PanelContainer Name="ProduceDivider" Visible="false" StyleClasses="LowDivider" />
|
||||
<Button Name="Print"
|
||||
TextAlign="Center"
|
||||
HorizontalExpand="True"
|
||||
Access="Public"
|
||||
Disabled="True"
|
||||
Margin="0 5 0 0"
|
||||
Text="{Loc 'plant-analyzer-print'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</controls:FancyWindow>
|
||||
210
Content.Client/Botany/PlantAnalyzer/PlantAnalyzerWindow.xaml.cs
Normal file
210
Content.Client/Botany/PlantAnalyzer/PlantAnalyzerWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
using System.Linq;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Botany.PlantAnalyzer;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Botany.PlantAnalyzer;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PlantAnalyzerWindow : FancyWindow
|
||||
{
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
|
||||
public PlantAnalyzerWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_prototypeManager = dependencies.Resolve<IPrototypeManager>();
|
||||
_gameTiming = dependencies.Resolve<IGameTiming>();
|
||||
}
|
||||
|
||||
public void Populate(PlantAnalyzerScannedUserMessage msg)
|
||||
{
|
||||
Print.Disabled = !msg.ScanMode.GetValueOrDefault(false)
|
||||
|| msg.PrintReadyAt.GetValueOrDefault(TimeSpan.MaxValue) > _gameTiming.CurTime
|
||||
|| msg.PlantData is null;
|
||||
|
||||
var target = _entityManager.GetEntity(msg.TargetEntity);
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Section 1: Icon and basic information.
|
||||
SpriteView.SetEntity(target.Value);
|
||||
SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
|
||||
NoDataIcon.Visible = !SpriteView.Visible;
|
||||
|
||||
ScanModeLabel.Text = msg.ScanMode.HasValue
|
||||
? msg.ScanMode.Value
|
||||
? Loc.GetString("health-analyzer-window-scan-mode-active")
|
||||
: Loc.GetString("health-analyzer-window-scan-mode-inactive")
|
||||
: Loc.GetString("health-analyzer-window-entity-unknown-text");
|
||||
ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
|
||||
|
||||
SeedLabel.Text = msg.PlantData == null
|
||||
? Loc.GetString("plant-analyzer-component-no-seed")
|
||||
: Loc.GetString(msg.PlantData.SeedDisplayName);
|
||||
|
||||
ContainerLabel.Text = _entityManager.HasComponent<MetaDataComponent>(target.Value)
|
||||
? Identity.Name(target.Value, _entityManager)
|
||||
: Loc.GetString("generic-unknown");
|
||||
|
||||
// Section 2: Information regarding the plant.
|
||||
if (msg.PlantData is not null)
|
||||
{
|
||||
Health.Text = msg.PlantData.Health.ToString("0.00");
|
||||
Endurance.Text = msg.PlantData.Endurance.ToString("0.00");
|
||||
Age.Text = msg.PlantData.Age.ToString("0.00");
|
||||
Lifespan.Text = msg.PlantData.Lifespan.ToString("0.00");
|
||||
|
||||
// These mostly exists to prevent shifting of the text.
|
||||
Dead.Visible = msg.PlantData.Dead;
|
||||
Alive.Visible = !Dead.Visible;
|
||||
|
||||
Unviable.Visible = !msg.PlantData.Viable;
|
||||
Mutating.Visible = msg.PlantData.Mutating;
|
||||
Kudzu.Visible = msg.PlantData.Kudzu;
|
||||
|
||||
PlantDataGrid.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlantDataGrid.Visible = false;
|
||||
}
|
||||
PlantDataTags.Visible = PlantDataGrid.Visible;
|
||||
PlantDataDivider.Visible = PlantDataGrid.Visible;
|
||||
|
||||
// Section 3: Input
|
||||
if (msg.TrayData is not null)
|
||||
{
|
||||
WaterLevelLabel.Text = msg.TrayData.WaterLevel.ToString("0.00");
|
||||
NutritionLevelLabel.Text = msg.TrayData.NutritionLevel.ToString("0.00");
|
||||
ToxinsLabel.Text = msg.TrayData.Toxins.ToString("0.00");
|
||||
PestLevelLabel.Text = msg.TrayData.PestLevel.ToString("0.00");
|
||||
WeedLevelLabel.Text = msg.TrayData.WeedLevel.ToString("0.00");
|
||||
|
||||
// Section 3.1: Tolerances part 1.
|
||||
if (msg.TolerancesData is not null)
|
||||
{
|
||||
GtFieldIfTolerances1.Text = ">";
|
||||
LtFieldIfTolerances1.Text = "<";
|
||||
|
||||
WaterConsumptionLabel.Text = msg.TolerancesData.WaterConsumption.ToString("0.00");
|
||||
NutritionConsumptionLabel.Text = msg.TolerancesData.NutrientConsumption.ToString("0.00");
|
||||
// Technically would be "x + epsilon" for toxin and pest.
|
||||
// But it makes no difference here since I only display two digits.
|
||||
ToxinsResistanceLabel.Text = msg.TolerancesData.ToxinsTolerance.ToString("0.00");
|
||||
PestResistanceLabel.Text = msg.TolerancesData.PestTolerance.ToString("0.00");
|
||||
WeedResistanceLabel.Text = msg.TolerancesData.WeedTolerance.ToString("0.00");
|
||||
}
|
||||
else
|
||||
{
|
||||
GtFieldIfTolerances1.Text = "";
|
||||
LtFieldIfTolerances1.Text = "";
|
||||
|
||||
WaterConsumptionLabel.Text = "";
|
||||
NutritionConsumptionLabel.Text = "";
|
||||
ToxinsResistanceLabel.Text = "";
|
||||
PestResistanceLabel.Text = "";
|
||||
WeedResistanceLabel.Text = "";
|
||||
}
|
||||
GtFieldIfTolerances2.Text = GtFieldIfTolerances1.Text;
|
||||
LtFieldIfTolerances2.Text = LtFieldIfTolerances1.Text;
|
||||
LtFieldIfTolerances3.Text = LtFieldIfTolerances1.Text;
|
||||
|
||||
ContainerGrid.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ContainerGrid.Visible = false;
|
||||
}
|
||||
ContainerDivider.Visible = ContainerGrid.Visible;
|
||||
|
||||
|
||||
// Section 3.5: They are putting chemicals in the water!
|
||||
if (msg.TrayData?.Chemicals != null)
|
||||
{
|
||||
var count = msg.TrayData.Chemicals.Count;
|
||||
var holder = ContainerLabel.Text;
|
||||
var chemicals = PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(msg.TrayData.Chemicals, _prototypeManager);
|
||||
if (count == 0)
|
||||
ChemicalsInWaterLabel.Text = Loc.GetString("plant-analyzer-soil-empty", ("holder", holder));
|
||||
else
|
||||
ChemicalsInWaterLabel.Text = Loc.GetString("plant-analyzer-soil", ("count", count), ("holder", holder), ("chemicals", chemicals));
|
||||
|
||||
ChemicalsInWaterBox.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ChemicalsInWaterBox.Visible = false;
|
||||
}
|
||||
ChemicalsInWaterDivider.Visible = ChemicalsInWaterBox.Visible;
|
||||
|
||||
// Section 4: Tolerances part 2.
|
||||
if (msg.TolerancesData is not null)
|
||||
{
|
||||
(string, string)[] parameters = [
|
||||
("seedName", SeedLabel.Text),
|
||||
("gases", PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(msg.TolerancesData.ConsumeGasses, _prototypeManager)),
|
||||
("kpa", msg.TolerancesData.IdealPressure.ToString("0.00")),
|
||||
("kpaTolerance", msg.TolerancesData.PressureTolerance.ToString("0.00")),
|
||||
("temp", msg.TolerancesData.IdealHeat.ToString("0.00")),
|
||||
("tempTolerance", msg.TolerancesData.HeatTolerance.ToString("0.00")),
|
||||
("lightLevel", msg.TolerancesData.IdealLight.ToString("0.00")),
|
||||
("lightTolerance", msg.TolerancesData.LightTolerance.ToString("0.00"))
|
||||
];
|
||||
EnvironmentLabel.Text = msg.TolerancesData.ConsumeGasses.Count == 0
|
||||
? msg.TolerancesData.IdealHeat - msg.TolerancesData.HeatTolerance <= 0f && msg.TolerancesData.IdealPressure - msg.TolerancesData.PressureTolerance <= 0f
|
||||
? Loc.GetString("plant-analyzer-component-environemt-void", [.. parameters])
|
||||
: Loc.GetString("plant-analyzer-component-environemt", [.. parameters])
|
||||
: Loc.GetString("plant-analyzer-component-environemt-gas", [.. parameters]);
|
||||
|
||||
EnvironmentBox.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
EnvironmentBox.Visible = false;
|
||||
}
|
||||
EnvironmentDivider.Visible = EnvironmentBox.Visible;
|
||||
|
||||
// Section 5: Output
|
||||
if (msg.ProduceData is not null)
|
||||
{
|
||||
var gases = PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(msg.ProduceData.ExudeGasses, _prototypeManager);
|
||||
var (produce, producePlural) = PlantAnalyzerLocalizationHelper.ProduceToLocalizedStrings(msg.ProduceData.Produce, _prototypeManager);
|
||||
var chemicals = PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(msg.ProduceData.Chemicals, _prototypeManager);
|
||||
|
||||
(string, object)[] parameters = [
|
||||
("yield", msg.ProduceData.Yield),
|
||||
("gasCount", msg.ProduceData.ExudeGasses.Count),
|
||||
("gases", gases),
|
||||
("potency", Loc.GetString(msg.ProduceData.Potency)),
|
||||
("seedless", msg.ProduceData.Seedless),
|
||||
("firstProduce", msg.ProduceData.Produce.FirstOrDefault() ?? ""),
|
||||
("produce", produce),
|
||||
("producePlural", producePlural),
|
||||
("chemCount", msg.ProduceData.Chemicals.Count),
|
||||
("chemicals", chemicals),
|
||||
("nothing", "")
|
||||
];
|
||||
|
||||
ProduceLabel.Text = Loc.GetString("plant-analyzer-output", [.. parameters]);
|
||||
ProduceBox.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ProduceBox.Visible = false;
|
||||
}
|
||||
ProduceDivider.Visible = ProduceBox.Visible;
|
||||
}
|
||||
}
|
||||
68
Content.Server/AbstractAnalyzer/AbstractAnalyzerComponent.cs
Normal file
68
Content.Server/AbstractAnalyzer/AbstractAnalyzerComponent.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.AbstractAnalyzer;
|
||||
|
||||
/// <summary>
|
||||
/// After scanning, retrieves the target Uid to use with its related UI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Requires <c>ItemToggleComponent</c>.
|
||||
/// </remarks>
|
||||
public abstract partial class AbstractAnalyzerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// When should the next update be sent for the patient
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Override this in your implementation with:
|
||||
///
|
||||
/// ```cs
|
||||
/// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
/// [AutoPausedField]
|
||||
/// public override TimeSpan NextUpdate { get; set; } = TimeSpan.Zero;
|
||||
/// ```
|
||||
/// </remarks>
|
||||
public abstract TimeSpan NextUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The delay between patient health updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to scan someone.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8);
|
||||
|
||||
/// <summary>
|
||||
/// Which entity has been scanned, for continuous updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? ScannedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum range in tiles at which the analyzer can receive continuous updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxScanRange = 2.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning begin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? ScanningBeginSound;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning end
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier ScanningEndSound = new SoundPathSpecifier("/Audio/Items/Medical/healthscanner.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show up the popup
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Silent;
|
||||
}
|
||||
193
Content.Server/AbstractAnalyzer/AbstractAnalyzerSystem.cs
Normal file
193
Content.Server/AbstractAnalyzer/AbstractAnalyzerSystem.cs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.AbstractAnalyzer;
|
||||
|
||||
public abstract class AbstractAnalyzerSystem<TAnalyzerComponent, TAnalyzerDoAfterEvent> : EntitySystem
|
||||
where TAnalyzerComponent : AbstractAnalyzerComponent
|
||||
where TAnalyzerDoAfterEvent : SimpleDoAfterEvent, new()
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PowerCellSystem _cell = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly ItemToggleSystem _toggle = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<TAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<TAnalyzerComponent, TAnalyzerDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<TAnalyzerComponent, EntGotInsertedIntoContainerMessage>(OnInsertedIntoContainer);
|
||||
SubscribeLocalEvent<TAnalyzerComponent, ItemToggledEvent>(OnToggled);
|
||||
SubscribeLocalEvent<TAnalyzerComponent, DroppedEvent>(OnDropped);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var analyzerQuery = EntityQueryEnumerator<TAnalyzerComponent, TransformComponent>();
|
||||
while (analyzerQuery.MoveNext(out var uid, out var component, out var transform))
|
||||
{
|
||||
//Update rate limited to 1 second
|
||||
if (component.NextUpdate > _timing.CurTime)
|
||||
continue;
|
||||
|
||||
if (component.ScannedEntity is not { } target)
|
||||
continue;
|
||||
|
||||
if (Deleted(target))
|
||||
{
|
||||
StopAnalyzingEntity((uid, component), target);
|
||||
continue;
|
||||
}
|
||||
|
||||
component.NextUpdate = _timing.CurTime + component.UpdateInterval;
|
||||
|
||||
//Get distance between analyzer and the scanned entity
|
||||
var targetCoordinates = Transform(target).Coordinates;
|
||||
if (!_transformSystem.InRange(targetCoordinates, transform.Coordinates, component.MaxScanRange))
|
||||
{
|
||||
//Range too far, disable updates
|
||||
StopAnalyzingEntity((uid, component), target);
|
||||
continue;
|
||||
}
|
||||
|
||||
UpdateScannedUser(uid, target, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the doafter for scanning
|
||||
/// </summary>
|
||||
private void OnAfterInteract(Entity<TAnalyzerComponent> uid, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Target == null || !args.CanReach || !ValidScanTarget(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
|
||||
return;
|
||||
|
||||
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
|
||||
|
||||
var doAfterCancelled = !_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new TAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
|
||||
{
|
||||
NeedHand = true,
|
||||
BreakOnMove = true,
|
||||
});
|
||||
|
||||
if (args.Target == args.User || doAfterCancelled || uid.Comp.Silent || args.Target is null)
|
||||
return;
|
||||
|
||||
if (ScanTargetPopupMessage(uid, args, out var msg))
|
||||
_popupSystem.PopupEntity(msg, args.Target.Value, args.Target.Value, PopupType.Medium);
|
||||
}
|
||||
|
||||
private void OnDoAfter(Entity<TAnalyzerComponent> uid, ref TAnalyzerDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
|
||||
return;
|
||||
|
||||
if (!uid.Comp.Silent)
|
||||
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
|
||||
|
||||
OpenUserInterface(args.User, uid);
|
||||
BeginAnalyzingEntity(uid, args.Target.Value);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off when placed into a storage item or moved between slots/hands
|
||||
/// </summary>
|
||||
private void OnInsertedIntoContainer(Entity<TAnalyzerComponent> uid, ref EntGotInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (uid.Comp.ScannedEntity is { })
|
||||
_toggle.TryDeactivate(uid.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable continuous updates once turned off
|
||||
/// </summary>
|
||||
private void OnToggled(Entity<TAnalyzerComponent> ent, ref ItemToggledEvent args)
|
||||
{
|
||||
if (!args.Activated && ent.Comp.ScannedEntity is { } target)
|
||||
StopAnalyzingEntity(ent, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off the analyser when dropped
|
||||
/// </summary>
|
||||
private void OnDropped(Entity<TAnalyzerComponent> uid, ref DroppedEvent args)
|
||||
{
|
||||
if (uid.Comp.ScannedEntity is { })
|
||||
_toggle.TryDeactivate(uid.Owner);
|
||||
}
|
||||
|
||||
private void OpenUserInterface(EntityUid user, EntityUid analyzer)
|
||||
{
|
||||
if (!_uiSystem.HasUi(analyzer, GetUiKey()))
|
||||
return;
|
||||
|
||||
_uiSystem.OpenUi(analyzer, GetUiKey(), user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark the entity as being analyzed, and link the analyzer to it
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that should receive the updates</param>
|
||||
/// <param name="target">The entity to start analyzing</param>
|
||||
private void BeginAnalyzingEntity(Entity<TAnalyzerComponent> analyzer, EntityUid target)
|
||||
{
|
||||
//Link the analyzer to the scanned entity
|
||||
analyzer.Comp.ScannedEntity = target;
|
||||
|
||||
_toggle.TryActivate(analyzer.Owner);
|
||||
|
||||
UpdateScannedUser(analyzer, target, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the analyzer from the active list, and remove the component if it has no active analyzers
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that's receiving the updates</param>
|
||||
/// <param name="target">The entity to analyze</param>
|
||||
private void StopAnalyzingEntity(Entity<TAnalyzerComponent> analyzer, EntityUid target)
|
||||
{
|
||||
//Unlink the analyzer
|
||||
analyzer.Comp.ScannedEntity = null;
|
||||
|
||||
_toggle.TryDeactivate(analyzer.Owner);
|
||||
|
||||
UpdateScannedUser(analyzer, target, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send an update for the target to the analyzer
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer</param>
|
||||
/// <param name="target">The entity being scanned</param>
|
||||
/// <param name="scanMode">True makes the UI show ACTIVE, False makes the UI show INACTIVE</param>
|
||||
public abstract void UpdateScannedUser(EntityUid analyzer, EntityUid target, bool scanMode);
|
||||
|
||||
/// <returns>A <see cref="Robust.Shared.Serialization.NetSerializableAttribute"/> byte enum key.</returns>
|
||||
protected abstract Enum GetUiKey();
|
||||
|
||||
/// <summary>
|
||||
/// The message the scan target recieves on scan.
|
||||
/// </summary>
|
||||
/// <returns>true if the message should be shown</returns>
|
||||
protected abstract bool ScanTargetPopupMessage(Entity<TAnalyzerComponent> uid, AfterInteractEvent args, [NotNullWhen(true)] out string? message);
|
||||
|
||||
/// <summary>
|
||||
/// Used to validate if a specific entity is a valid target for a specific analyzer.
|
||||
/// </summary>
|
||||
protected abstract bool ValidScanTarget(EntityUid? target);
|
||||
}
|
||||
43
Content.Server/Botany/Components/PlantAnalyzerComponent.cs
Normal file
43
Content.Server/Botany/Components/PlantAnalyzerComponent.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using Content.Server.AbstractAnalyzer;
|
||||
using Content.Server.Botany.Systems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Botany.Components;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
[Access(typeof(PlantAnalyzerSystem))]
|
||||
public sealed partial class PlantAnalyzerComponent : AbstractAnalyzerComponent
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[AutoPausedField]
|
||||
public override TimeSpan NextUpdate { get; set; } = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// When will the analyzer be ready to print again?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public TimeSpan PrintReadyAt = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// How often can the analyzer print?
|
||||
/// </summary>
|
||||
[DataField("printCooldown")]
|
||||
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// The sound that's played when the analyzer prints off a report.
|
||||
/// </summary>
|
||||
[DataField("soundPrint")]
|
||||
public SoundSpecifier SoundPrint = new SoundPathSpecifier("/Audio/Machines/short_print_and_rip.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// What the machine will print.
|
||||
/// </summary>
|
||||
[DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string MachineOutput = "PlantAnalyzerReportPaper";
|
||||
}
|
||||
|
|
@ -148,17 +148,7 @@ public sealed partial class BotanySystem : EntitySystem
|
|||
|
||||
public IEnumerable<EntityUid> GenerateProduct(SeedData proto, EntityCoordinates position, int yieldMod = 1)
|
||||
{
|
||||
var totalYield = 0;
|
||||
if (proto.Yield > -1)
|
||||
{
|
||||
if (yieldMod < 0)
|
||||
totalYield = proto.Yield;
|
||||
else
|
||||
totalYield = proto.Yield * yieldMod;
|
||||
|
||||
totalYield = Math.Max(1, totalYield);
|
||||
}
|
||||
|
||||
var totalYield = CalculateTotalYield(proto.Yield, yieldMod);
|
||||
var products = new List<EntityUid>();
|
||||
|
||||
if (totalYield > 1 || proto.HarvestRepeat != HarvestType.NoRepeat)
|
||||
|
|
@ -196,5 +186,20 @@ public sealed partial class BotanySystem : EntitySystem
|
|||
return !proto.Ligneous || proto.Ligneous && held != null && HasComp<SharpComponent>(held);
|
||||
}
|
||||
|
||||
public static int CalculateTotalYield(int yield, int yieldMod)
|
||||
{
|
||||
var totalYield = 0;
|
||||
if (yield > -1)
|
||||
{
|
||||
if (yieldMod < 0)
|
||||
totalYield = yield;
|
||||
else
|
||||
totalYield = yield * yieldMod;
|
||||
|
||||
totalYield = Math.Max(1, totalYield);
|
||||
}
|
||||
return totalYield;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
206
Content.Server/Botany/Systems/PlantAnalyzerSystem.cs
Normal file
206
Content.Server/Botany/Systems/PlantAnalyzerSystem.cs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.AbstractAnalyzer;
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Botany.PlantAnalyzer;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Labels.EntitySystems;
|
||||
using Content.Shared.Paper;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Botany.Systems;
|
||||
|
||||
public sealed class PlantAnalyzerSystem : AbstractAnalyzerSystem<PlantAnalyzerComponent, PlantAnalyzerDoAfterEvent>
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly PaperSystem _paperSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedLabelSystem _labelSystem = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PlantAnalyzerComponent, PlantAnalyzerPrintMessage>(OnPrint);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void UpdateScannedUser(EntityUid analyzer, EntityUid target, bool scanMode)
|
||||
{
|
||||
if (!_uiSystem.HasUi(analyzer, PlantAnalyzerUiKey.Key))
|
||||
return;
|
||||
|
||||
if (!ValidScanTarget(target))
|
||||
return;
|
||||
|
||||
if (!_entityManager.TryGetComponent<PlantAnalyzerComponent>(analyzer, out var analyzerComponent))
|
||||
return;
|
||||
|
||||
_uiSystem.ServerSendUiMessage(analyzer, PlantAnalyzerUiKey.Key, GatherData(analyzerComponent, scanMode, target: target));
|
||||
}
|
||||
|
||||
private PlantAnalyzerScannedUserMessage GatherData(PlantAnalyzerComponent analyzer, bool? scanMode = null, EntityUid? target = null)
|
||||
{
|
||||
target ??= analyzer.ScannedEntity;
|
||||
PlantAnalyzerPlantData? plantData = null;
|
||||
PlantAnalyzerTrayData? trayData = null;
|
||||
PlantAnalyzerTolerancesData? tolerancesData = null;
|
||||
PlantAnalyzerProduceData? produceData = null;
|
||||
if (_entityManager.TryGetComponent<PlantHolderComponent>(target, out var plantHolder))
|
||||
{
|
||||
if (plantHolder.Seed is not null)
|
||||
{
|
||||
plantData = new PlantAnalyzerPlantData(
|
||||
seedDisplayName: plantHolder.Seed.DisplayName,
|
||||
health: plantHolder.Health,
|
||||
endurance: plantHolder.Seed.Endurance,
|
||||
age: plantHolder.Age,
|
||||
lifespan: plantHolder.Seed.Lifespan,
|
||||
dead: plantHolder.Dead,
|
||||
viable: plantHolder.Seed.Viable,
|
||||
mutating: plantHolder.MutationLevel > 0f,
|
||||
kudzu: plantHolder.Seed.TurnIntoKudzu
|
||||
);
|
||||
tolerancesData = new PlantAnalyzerTolerancesData(
|
||||
waterConsumption: plantHolder.Seed.WaterConsumption,
|
||||
nutrientConsumption: plantHolder.Seed.NutrientConsumption,
|
||||
toxinsTolerance: plantHolder.Seed.ToxinsTolerance,
|
||||
pestTolerance: plantHolder.Seed.PestTolerance,
|
||||
weedTolerance: plantHolder.Seed.WeedTolerance,
|
||||
lowPressureTolerance: plantHolder.Seed.LowPressureTolerance,
|
||||
highPressureTolerance: plantHolder.Seed.HighPressureTolerance,
|
||||
idealHeat: plantHolder.Seed.IdealHeat,
|
||||
heatTolerance: plantHolder.Seed.HeatTolerance,
|
||||
idealLight: plantHolder.Seed.IdealLight,
|
||||
lightTolerance: plantHolder.Seed.LightTolerance,
|
||||
consumeGasses: [.. plantHolder.Seed.ConsumeGasses.Keys]
|
||||
);
|
||||
produceData = new PlantAnalyzerProduceData(
|
||||
yield: plantHolder.Seed.ProductPrototypes.Count == 0 ? 0 : BotanySystem.CalculateTotalYield(plantHolder.Seed.Yield, plantHolder.YieldMod),
|
||||
potency: plantHolder.Seed.Potency,
|
||||
chemicals: [.. plantHolder.Seed.Chemicals.Keys],
|
||||
produce: plantHolder.Seed.ProductPrototypes,
|
||||
exudeGasses: [.. plantHolder.Seed.ExudeGasses.Keys],
|
||||
seedless: plantHolder.Seed.Seedless
|
||||
);
|
||||
}
|
||||
trayData = new PlantAnalyzerTrayData(
|
||||
waterLevel: plantHolder.WaterLevel,
|
||||
nutritionLevel: plantHolder.NutritionLevel,
|
||||
toxins: plantHolder.Toxins,
|
||||
pestLevel: plantHolder.PestLevel,
|
||||
weedLevel: plantHolder.WeedLevel,
|
||||
chemicals: plantHolder.SoilSolution?.Comp.Solution.Contents.Select(r => r.Reagent.Prototype).ToList()
|
||||
);
|
||||
}
|
||||
|
||||
return new PlantAnalyzerScannedUserMessage(
|
||||
GetNetEntity(target),
|
||||
scanMode,
|
||||
plantData,
|
||||
trayData,
|
||||
tolerancesData,
|
||||
produceData,
|
||||
analyzer.PrintReadyAt
|
||||
);
|
||||
}
|
||||
|
||||
private void OnPrint(EntityUid uid, PlantAnalyzerComponent component, PlantAnalyzerPrintMessage args)
|
||||
{
|
||||
var user = args.Actor;
|
||||
|
||||
if (_gameTiming.CurTime < component.PrintReadyAt)
|
||||
{
|
||||
// This shouldn't occur due to the UI guarding against it, but
|
||||
// if it does, tell the user why nothing happened.
|
||||
_popupSystem.PopupEntity(Loc.GetString("forensic-scanner-printer-not-ready"), uid, user);
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn a piece of paper.
|
||||
var printed = EntityManager.SpawnEntity(component.MachineOutput, Transform(uid).Coordinates);
|
||||
_handsSystem.PickupOrDrop(args.Actor, printed, checkActionBlocker: false);
|
||||
|
||||
if (!TryComp<PaperComponent>(printed, out var paperComp))
|
||||
{
|
||||
Log.Error("Printed paper did not have PaperComponent.");
|
||||
return;
|
||||
}
|
||||
|
||||
var data = GatherData(component);
|
||||
var missingData = Loc.GetString("plant-analyzer-printout-missing");
|
||||
|
||||
var seedName = data.PlantData is not null ? Loc.GetString(data.PlantData.SeedDisplayName) : null;
|
||||
(string, object)[] parameters = [
|
||||
("seedName", seedName ?? missingData),
|
||||
("produce", data.ProduceData is not null ? PlantAnalyzerLocalizationHelper.ProduceToLocalizedStrings(data.ProduceData.Produce, _prototypeManager).Plural : missingData),
|
||||
("water", data.TolerancesData?.WaterConsumption.ToString("0.00") ?? missingData),
|
||||
("nutrients", data.TolerancesData?.NutrientConsumption.ToString("0.00") ?? missingData),
|
||||
("toxins", data.TolerancesData?.ToxinsTolerance.ToString("0.00") ?? missingData),
|
||||
("pests", data.TolerancesData?.PestTolerance.ToString("0.00") ?? missingData),
|
||||
("weeds", data.TolerancesData?.WeedTolerance.ToString("0.00") ?? missingData),
|
||||
("gasesIn", data.TolerancesData is not null ? PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(data.TolerancesData.ConsumeGasses, _prototypeManager) : missingData),
|
||||
("kpa", data.TolerancesData?.IdealPressure.ToString("0.00") ?? missingData),
|
||||
("kpaTolerance", data.TolerancesData?.PressureTolerance.ToString("0.00") ?? missingData),
|
||||
("temp", data.TolerancesData?.IdealHeat.ToString("0.00") ?? missingData),
|
||||
("tempTolerance", data.TolerancesData?.HeatTolerance.ToString("0.00") ?? missingData),
|
||||
("lightLevel", data.TolerancesData?.IdealLight.ToString("0.00") ?? missingData),
|
||||
("lightTolerance", data.TolerancesData?.LightTolerance.ToString("0.00") ?? missingData),
|
||||
("yield", data.ProduceData?.Yield ?? -1),
|
||||
("potency", data.ProduceData is not null ? Loc.GetString(data.ProduceData.Potency) : missingData),
|
||||
("chemicals", data.ProduceData is not null ? PlantAnalyzerLocalizationHelper.ChemicalsToLocalizedStrings(data.ProduceData.Chemicals, _prototypeManager) : missingData),
|
||||
("gasesOut", data.ProduceData is not null ? PlantAnalyzerLocalizationHelper.GasesToLocalizedStrings(data.ProduceData.ExudeGasses, _prototypeManager) : missingData),
|
||||
("endurance", data.PlantData?.Endurance.ToString("0.00") ?? missingData),
|
||||
("lifespan", data.PlantData?.Lifespan.ToString("0.00") ?? missingData),
|
||||
("seeds", data.ProduceData is not null ? (data.ProduceData.Seedless ? "no" : "yes") : "other"),
|
||||
("viable", data.PlantData is not null ? (data.PlantData.Viable ? "yes" : "no") : "other"),
|
||||
("kudzu", data.PlantData is not null ? (data.PlantData.Kudzu ? "yes" : "no") : "other"),
|
||||
("indent", " "),
|
||||
("nl", "\n")
|
||||
];
|
||||
|
||||
_paperSystem.SetContent((printed, paperComp), Loc.GetString($"plant-analyzer-printout", [.. parameters]));
|
||||
_labelSystem.Label(printed, seedName);
|
||||
_audioSystem.PlayPvs(component.SoundPrint, uid,
|
||||
AudioParams.Default
|
||||
.WithVariation(0.25f)
|
||||
.WithVolume(3f)
|
||||
.WithRolloffFactor(2.8f)
|
||||
.WithMaxDistance(4.5f));
|
||||
|
||||
component.PrintReadyAt = _gameTiming.CurTime + component.PrintCooldown;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Enum GetUiKey()
|
||||
{
|
||||
return PlantAnalyzerUiKey.Key;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool ScanTargetPopupMessage(Entity<PlantAnalyzerComponent> uid, AfterInteractEvent args, [NotNullWhen(true)] out string? message)
|
||||
{
|
||||
message = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool ValidScanTarget(EntityUid? target)
|
||||
{
|
||||
return HasComp<PlantHolderComponent>(target);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ public sealed partial class PlantAdjustPests : PlantAdjustAttribute
|
|||
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager))
|
||||
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false))
|
||||
return;
|
||||
|
||||
plantHolderComp.PestLevel += Amount;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public sealed partial class PlantAdjustToxins : PlantAdjustAttribute
|
|||
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager))
|
||||
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false))
|
||||
return;
|
||||
|
||||
plantHolderComp.Toxins += Amount;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public sealed partial class PlantAdjustWeeds : PlantAdjustAttribute
|
|||
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager))
|
||||
if (!CanMetabolize(args.TargetEntity, out var plantHolderComp, args.EntityManager, mustHaveAlivePlant: false))
|
||||
return;
|
||||
|
||||
plantHolderComp.WeedLevel += Amount;
|
||||
|
|
|
|||
|
|
@ -1,68 +1,18 @@
|
|||
using Robust.Shared.Audio;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Server.AbstractAnalyzer;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Server.Medical.Components;
|
||||
|
||||
/// <summary>
|
||||
/// After scanning, retrieves the target Uid to use with its related UI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Requires <c>ItemToggleComponent</c>.
|
||||
/// </remarks>
|
||||
/// <inheritdoc/>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
[Access(typeof(HealthAnalyzerSystem), typeof(CryoPodSystem))]
|
||||
public sealed partial class HealthAnalyzerComponent : Component
|
||||
public sealed partial class HealthAnalyzerComponent : AbstractAnalyzerComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// When should the next update be sent for the patient
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[AutoPausedField]
|
||||
public TimeSpan NextUpdate = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The delay between patient health updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to scan someone.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan ScanDelay = TimeSpan.FromSeconds(0.8);
|
||||
|
||||
/// <summary>
|
||||
/// Which entity has been scanned, for continuous updates
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? ScannedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum range in tiles at which the analyzer can receive continuous updates, a value of null will be infinite range
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float? MaxScanRange = 2.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning begin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? ScanningBeginSound;
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on scanning end
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier ScanningEndSound = new SoundPathSpecifier("/Audio/Items/Medical/healthscanner.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show up the popup
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Silent;
|
||||
public override TimeSpan NextUpdate { get; set; } = TimeSpan.Zero;
|
||||
|
||||
[DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageContainerPrototype>))]
|
||||
public List<string>? DamageContainers;
|
||||
|
|
|
|||
|
|
@ -1,207 +1,43 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.AbstractAnalyzer;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Medical.Components;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Content.Shared.Item.ItemToggle.Components;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Medical;
|
||||
|
||||
public sealed class HealthAnalyzerSystem : EntitySystem
|
||||
public sealed class HealthAnalyzerSystem : AbstractAnalyzerSystem<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PowerCellSystem _cell = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly ItemToggleSystem _toggle = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, HealthAnalyzerDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, EntGotInsertedIntoContainerMessage>(OnInsertedIntoContainer);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, ItemToggledEvent>(OnToggled);
|
||||
SubscribeLocalEvent<HealthAnalyzerComponent, DroppedEvent>(OnDropped);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var analyzerQuery = EntityQueryEnumerator<HealthAnalyzerComponent, TransformComponent>();
|
||||
while (analyzerQuery.MoveNext(out var uid, out var component, out var transform))
|
||||
{
|
||||
//Update rate limited to 1 second
|
||||
if (component.NextUpdate > _timing.CurTime)
|
||||
continue;
|
||||
|
||||
if (component.ScannedEntity is not {} patient)
|
||||
continue;
|
||||
|
||||
if (Deleted(patient))
|
||||
{
|
||||
StopAnalyzingEntity((uid, component), patient);
|
||||
continue;
|
||||
}
|
||||
|
||||
component.NextUpdate = _timing.CurTime + component.UpdateInterval;
|
||||
|
||||
//Get distance between health analyzer and the scanned entity
|
||||
//null is infinite range
|
||||
var patientCoordinates = Transform(patient).Coordinates;
|
||||
if (component.MaxScanRange != null && !_transformSystem.InRange(patientCoordinates, transform.Coordinates, component.MaxScanRange.Value))
|
||||
{
|
||||
//Range too far, disable updates
|
||||
StopAnalyzingEntity((uid, component), patient);
|
||||
continue;
|
||||
}
|
||||
|
||||
UpdateScannedUser(uid, patient, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger the doafter for scanning
|
||||
/// </summary>
|
||||
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Target == null ||
|
||||
!args.CanReach ||
|
||||
!TryComp<DamageableComponent>(args.Target, out var damageableComponent) || // Sunrise-Edit
|
||||
!HasComp<MobStateComponent>(args.Target) ||
|
||||
!_cell.HasDrawCharge(uid, user: args.User))
|
||||
return;
|
||||
|
||||
// Sunrise-Start
|
||||
if (uid.Comp.DamageContainers is not null &&
|
||||
damageableComponent.DamageContainerID is not null &&
|
||||
!uid.Comp.DamageContainers.Contains(damageableComponent.DamageContainerID))
|
||||
return;
|
||||
// Sunrise-End
|
||||
|
||||
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
|
||||
|
||||
var doAfterCancelled = !_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, uid.Comp.ScanDelay, new HealthAnalyzerDoAfterEvent(), uid, target: args.Target, used: uid)
|
||||
{
|
||||
NeedHand = args.NeedHand, // Sunrise-Edit
|
||||
BreakOnMove = true,
|
||||
});
|
||||
|
||||
if (args.Target == args.User || doAfterCancelled || uid.Comp.Silent)
|
||||
return;
|
||||
|
||||
var msg = Loc.GetString("health-analyzer-popup-scan-target", ("user", Identity.Entity(args.User, EntityManager)));
|
||||
_popupSystem.PopupEntity(msg, args.Target.Value, args.Target.Value, PopupType.Medium);
|
||||
}
|
||||
|
||||
private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
|
||||
return;
|
||||
|
||||
if (!uid.Comp.Silent)
|
||||
_audio.PlayPvs(uid.Comp.ScanningEndSound, uid);
|
||||
|
||||
OpenUserInterface(args.User, uid);
|
||||
BeginAnalyzingEntity(uid, args.Target.Value);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off when placed into a storage item or moved between slots/hands
|
||||
/// </summary>
|
||||
private void OnInsertedIntoContainer(Entity<HealthAnalyzerComponent> uid, ref EntGotInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (uid.Comp.ScannedEntity is { } patient)
|
||||
_toggle.TryDeactivate(uid.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable continuous updates once turned off
|
||||
/// </summary>
|
||||
private void OnToggled(Entity<HealthAnalyzerComponent> ent, ref ItemToggledEvent args)
|
||||
{
|
||||
if (!args.Activated && ent.Comp.ScannedEntity is { } patient)
|
||||
StopAnalyzingEntity(ent, patient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off the analyser when dropped
|
||||
/// </summary>
|
||||
private void OnDropped(Entity<HealthAnalyzerComponent> uid, ref DroppedEvent args)
|
||||
{
|
||||
if (uid.Comp.ScannedEntity is { } patient)
|
||||
_toggle.TryDeactivate(uid.Owner);
|
||||
}
|
||||
|
||||
private void OpenUserInterface(EntityUid user, EntityUid analyzer)
|
||||
{
|
||||
if (!_uiSystem.HasUi(analyzer, HealthAnalyzerUiKey.Key))
|
||||
return;
|
||||
|
||||
_uiSystem.OpenUi(analyzer, HealthAnalyzerUiKey.Key, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark the entity as having its health analyzed, and link the analyzer to it
|
||||
/// </summary>
|
||||
/// <param name="healthAnalyzer">The health analyzer that should receive the updates</param>
|
||||
/// <param name="target">The entity to start analyzing</param>
|
||||
private void BeginAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
|
||||
{
|
||||
//Link the health analyzer to the scanned entity
|
||||
healthAnalyzer.Comp.ScannedEntity = target;
|
||||
|
||||
_toggle.TryActivate(healthAnalyzer.Owner);
|
||||
|
||||
UpdateScannedUser(healthAnalyzer, target, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the analyzer from the active list, and remove the component if it has no active analyzers
|
||||
/// </summary>
|
||||
/// <param name="healthAnalyzer">The health analyzer that's receiving the updates</param>
|
||||
/// <param name="target">The entity to analyze</param>
|
||||
private void StopAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
|
||||
{
|
||||
//Unlink the analyzer
|
||||
healthAnalyzer.Comp.ScannedEntity = null;
|
||||
|
||||
_toggle.TryDeactivate(healthAnalyzer.Owner);
|
||||
|
||||
UpdateScannedUser(healthAnalyzer, target, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send an update for the target to the healthAnalyzer
|
||||
/// </summary>
|
||||
/// <param name="healthAnalyzer">The health analyzer</param>
|
||||
/// <param name="target">The entity being scanned</param>
|
||||
/// <param name="scanMode">True makes the UI show ACTIVE, False makes the UI show INACTIVE</param>
|
||||
public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode)
|
||||
/// <inheritdoc/>
|
||||
public override void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool scanMode)
|
||||
{
|
||||
if (!_uiSystem.HasUi(healthAnalyzer, HealthAnalyzerUiKey.Key))
|
||||
return;
|
||||
|
||||
if (!HasComp<DamageableComponent>(target))
|
||||
if (!TryComp<DamageableComponent>(target, out var damageableComponent)) // Sunrise-Edit
|
||||
return;
|
||||
|
||||
// Sunrise-Start
|
||||
if (!TryComp<HealthAnalyzerComponent>(healthAnalyzer, out var healthAnalyzerComp))
|
||||
return;
|
||||
|
||||
if (healthAnalyzerComp.DamageContainers is not null &&
|
||||
damageableComponent.DamageContainerID is not null &&
|
||||
!healthAnalyzerComp.DamageContainers.Contains(damageableComponent.DamageContainerID))
|
||||
return;
|
||||
// Sunrise-End
|
||||
|
||||
var bodyTemperature = float.NaN;
|
||||
|
||||
if (TryComp<TemperatureComponent>(target, out var temp))
|
||||
|
|
@ -231,4 +67,20 @@ public sealed class HealthAnalyzerSystem : EntitySystem
|
|||
unrevivable
|
||||
));
|
||||
}
|
||||
|
||||
protected override Enum GetUiKey()
|
||||
{
|
||||
return HealthAnalyzerUiKey.Key;
|
||||
}
|
||||
|
||||
protected override bool ScanTargetPopupMessage(Entity<HealthAnalyzerComponent> uid, AfterInteractEvent args, [NotNullWhen(true)] out string? message)
|
||||
{
|
||||
message = Loc.GetString("health-analyzer-popup-scan-target", ("user", Identity.Entity(args.User, EntityManager)));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool ValidScanTarget(EntityUid? target)
|
||||
{
|
||||
return HasComp<MobStateComponent>(target);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
using Content.Shared.DoAfter;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Botany.PlantAnalyzer;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class PlantAnalyzerDoAfterEvent : SimpleDoAfterEvent
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Botany.PlantAnalyzer;
|
||||
|
||||
public sealed class PlantAnalyzerLocalizationHelper
|
||||
{
|
||||
public static string GasesToLocalizedStrings(List<Gas> gases, IPrototypeManager protMan)
|
||||
{
|
||||
if (gases.Count == 0)
|
||||
return "";
|
||||
|
||||
List<int> gasIds = [];
|
||||
foreach (var gas in gases)
|
||||
gasIds.Add((int)gas);
|
||||
|
||||
List<string> gasesLoc = [];
|
||||
foreach (var gas in protMan.EnumeratePrototypes<GasPrototype>())
|
||||
if (gasIds.Contains(int.Parse(gas.ID)))
|
||||
gasesLoc.Add(Loc.GetString(gas.Name));
|
||||
|
||||
return ContentLocalizationManager.FormatList(gasesLoc);
|
||||
}
|
||||
|
||||
public static string ChemicalsToLocalizedStrings(List<string> ids, IPrototypeManager protMan)
|
||||
{
|
||||
if (ids.Count == 0)
|
||||
return "";
|
||||
|
||||
List<string> locStrings = [];
|
||||
foreach (var id in ids)
|
||||
locStrings.Add(protMan.TryIndex<ReagentPrototype>(id, out var prototype) ? prototype.LocalizedName : id);
|
||||
|
||||
return ContentLocalizationManager.FormatList(locStrings);
|
||||
}
|
||||
|
||||
public static (string Singular, string Plural) ProduceToLocalizedStrings(List<string> ids, IPrototypeManager protMan)
|
||||
{
|
||||
if (ids.Count == 0)
|
||||
return ("", "");
|
||||
|
||||
List<string> singularStrings = [];
|
||||
List<string> pluralStrings = [];
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var singular = protMan.TryIndex<EntityPrototype>(id, out var prototype) ? prototype.Name : id;
|
||||
var plural = Loc.GetString("plant-analyzer-produce-plural", ("thing", singular));
|
||||
|
||||
singularStrings.Add(singular);
|
||||
pluralStrings.Add(plural);
|
||||
}
|
||||
|
||||
return (
|
||||
ContentLocalizationManager.FormatListToOr(singularStrings),
|
||||
ContentLocalizationManager.FormatListToOr(pluralStrings)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Botany.PlantAnalyzer;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PlantAnalyzerScannedUserMessage(NetEntity? targetEntity, bool? scanMode, PlantAnalyzerPlantData? plantData, PlantAnalyzerTrayData? trayData, PlantAnalyzerTolerancesData? tolerancesData, PlantAnalyzerProduceData? produceData, TimeSpan? printReadyAt) : BoundUserInterfaceMessage
|
||||
{
|
||||
public readonly NetEntity? TargetEntity = targetEntity;
|
||||
public bool? ScanMode = scanMode;
|
||||
public PlantAnalyzerPlantData? PlantData = plantData;
|
||||
public PlantAnalyzerTrayData? TrayData = trayData;
|
||||
public PlantAnalyzerTolerancesData? TolerancesData = tolerancesData;
|
||||
public PlantAnalyzerProduceData? ProduceData = produceData;
|
||||
public readonly TimeSpan? PrintReadyAt = printReadyAt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Everything that is kept independed of a given plant/seed.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PlantAnalyzerTrayData(float waterLevel, float nutritionLevel, float toxins, float pestLevel, float weedLevel, List<string>? chemicals)
|
||||
{
|
||||
public float WaterLevel = waterLevel;
|
||||
public float NutritionLevel = nutritionLevel;
|
||||
public float Toxins = toxins;
|
||||
public float PestLevel = pestLevel;
|
||||
public float WeedLevel = weedLevel;
|
||||
public List<string>? Chemicals = chemicals;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// All the information to keep the plant alive.
|
||||
/// Which is most of the "Tolerances" reagion plus the gases it may need.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PlantAnalyzerTolerancesData(float nutrientConsumption, float waterConsumption, float idealHeat, float heatTolerance, float idealLight, float lightTolerance, float toxinsTolerance, float lowPressureTolerance, float highPressureTolerance, float pestTolerance, float weedTolerance, List<Gas> consumeGasses)
|
||||
{
|
||||
public float WaterConsumption = waterConsumption;
|
||||
public float NutrientConsumption = nutrientConsumption;
|
||||
public float ToxinsTolerance = toxinsTolerance;
|
||||
public float PestTolerance = pestTolerance;
|
||||
public float WeedTolerance = weedTolerance;
|
||||
public float IdealPressure = (lowPressureTolerance + highPressureTolerance) / 2;
|
||||
public float PressureTolerance = (lowPressureTolerance + highPressureTolerance) / 2 - lowPressureTolerance;
|
||||
public float IdealHeat = idealHeat;
|
||||
public float HeatTolerance = heatTolerance;
|
||||
public float IdealLight = idealLight;
|
||||
public float LightTolerance = lightTolerance;
|
||||
public List<Gas> ConsumeGasses = consumeGasses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the plant inside the tray.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PlantAnalyzerPlantData(string seedDisplayName, float health, float endurance, float age, float lifespan, bool dead, bool viable, bool mutating, bool kudzu)
|
||||
{
|
||||
public string SeedDisplayName = seedDisplayName;
|
||||
public float Health = health;
|
||||
public float Endurance = endurance;
|
||||
public float Age = age;
|
||||
public float Lifespan = lifespan;
|
||||
public bool Dead = dead;
|
||||
public bool Viable = viable;
|
||||
public bool Mutating = mutating;
|
||||
public bool Kudzu = kudzu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the output of a plant (produce and gas).
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PlantAnalyzerProduceData(int yield, float potency, List<string> chemicals, List<string> produce, List<Gas> exudeGasses, bool seedless)
|
||||
{
|
||||
public int Yield = yield;
|
||||
public string Potency = ObscurePotency(potency);
|
||||
public List<string> Chemicals = chemicals;
|
||||
public List<string> Produce = produce;
|
||||
public List<Gas> ExudeGasses = exudeGasses;
|
||||
public bool Seedless = seedless;
|
||||
|
||||
private static string ObscurePotency(float potency)
|
||||
{
|
||||
var potencyFtl = "plant-analyzer-potency-";
|
||||
if (potency <= 5) // 5 should still be tiny
|
||||
potencyFtl += "tiny";
|
||||
else if (potency < 10) // 10 should be below-average
|
||||
potencyFtl += "small";
|
||||
else if (potency < 15)
|
||||
potencyFtl += "below-average";
|
||||
else if (potency < 20)
|
||||
potencyFtl += "average";
|
||||
else if (potency <= 25) // 25 is the highest starting value
|
||||
potencyFtl += "above-average";
|
||||
else if (potency < 30)
|
||||
potencyFtl += "large";
|
||||
else if (potency < 40)
|
||||
potencyFtl += "huge";
|
||||
else if (potency < 50)
|
||||
potencyFtl += "gigantic";
|
||||
else if (potency < 60)
|
||||
potencyFtl += "ludicrous";
|
||||
else
|
||||
potencyFtl += "immeasurable";
|
||||
|
||||
return potencyFtl;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PlantAnalyzerPrintMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Botany.PlantAnalyzer;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum PlantAnalyzerUiKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
plant-analyzer-component-no-seed = no plant found
|
||||
|
||||
plant-analyzer-component-health = Health:
|
||||
plant-analyzer-component-age = Age:
|
||||
plant-analyzer-component-water = Water:
|
||||
plant-analyzer-component-nutrition = Nutrition:
|
||||
plant-analyzer-component-toxins = Toxins:
|
||||
plant-analyzer-component-pests = Pests:
|
||||
plant-analyzer-component-weeds = Weeds:
|
||||
|
||||
plant-analyzer-component-alive = [color=green]ALIVE[color]
|
||||
plant-analyzer-component-dead = [color=red]DEAD[color]
|
||||
plant-analyzer-component-unviable = [color=red]UNVIABLE[color]
|
||||
plant-analyzer-component-mutating = [color=#00ff5f]MUTATING[color]
|
||||
plant-analyzer-component-kudzu = [color=red]KUDZU[color]
|
||||
|
||||
plant-analyzer-soil = There is some [color=white]{$chemicals}[/color] in this {$holder} that {$count ->
|
||||
[one]has
|
||||
*[other]have
|
||||
} not been absorbed.
|
||||
plant-analyzer-soil-empty = There are no unabsorbed chemicals in this {$holder}.
|
||||
|
||||
plant-analyzer-component-environemt = This [color=green]{$seedName}[/color] requires an atmosphere at a pressure level of [color=lightblue]{$kpa}kPa ± {$kpaTolerance}kPa[/color], temperature of [color=lightsalmon]{$temp}°k ± {$tempTolerance}°k[/color] and a light level of [color=white]{$lightLevel} ± {$lightTolerance}[/color].
|
||||
plant-analyzer-component-environemt-void = This [color=green]{$seedName}[/color] has to be grown [bolditalic]in the vacuum of space[/bolditalic] at a light level of [color=white]{$lightLevel} ± {$lightTolerance}[/color].
|
||||
plant-analyzer-component-environemt-gas = This [color=green]{$seedName}[/color] requires an atmosphere containing [bold]{$gases}[/bold] at a pressure level of [color=lightblue]{$kpa}kPa ± {$kpaTolerance}kPa[/color], temperature of [color=lightsalmon]{$temp}°k ± {$tempTolerance}°k[/color] and a light level of [color=white]{$lightLevel} ± {$lightTolerance}[/color].
|
||||
|
||||
plant-analyzer-produce-plural = {MAKEPLURAL($thing)}
|
||||
plant-analyzer-output = {$yield ->
|
||||
[0]{$gasCount ->
|
||||
[0]The only thing it seems to do is consume water and nutrients.
|
||||
*[other]The only thing it seems to do is turn water and nutrients into [bold]{$gases}[/bold].
|
||||
}
|
||||
*[other]It has [color=lightgreen]{$yield} {$potency}[/color]{$seedless ->
|
||||
[true]{" "}but [color=red]seedless[/color]
|
||||
*[false]{$nothing}
|
||||
}{" "}{$yield ->
|
||||
[one]flower
|
||||
*[other]flowers
|
||||
}{" "}that{$gasCount ->
|
||||
[0]{$nothing}
|
||||
*[other]{$yield ->
|
||||
[one]{" "}emits
|
||||
*[other]{" "}emit
|
||||
}{" "}[bold]{$gases}[/bold] and
|
||||
}{" "}will turn into{$yield ->
|
||||
[one]{" "}{INDEFINITE($firstProduce)} [color=#a4885c]{$produce}[/color]
|
||||
*[other]{" "}[color=#a4885c]{$producePlural}[/color]
|
||||
}.{$chemCount ->
|
||||
[0]{$nothing}
|
||||
*[other]{" "}There are trace amounts of [color=white]{$chemicals}[/color] in its stem.
|
||||
}
|
||||
}
|
||||
|
||||
plant-analyzer-potency-tiny = tiny
|
||||
plant-analyzer-potency-small = small
|
||||
plant-analyzer-potency-below-average = below-average sized
|
||||
plant-analyzer-potency-average = average sized
|
||||
plant-analyzer-potency-above-average = above-average sized
|
||||
plant-analyzer-potency-large = rather large
|
||||
plant-analyzer-potency-huge = huge
|
||||
plant-analyzer-potency-gigantic = gigantic
|
||||
plant-analyzer-potency-ludicrous = ludicrously large
|
||||
plant-analyzer-potency-immeasurable = immeasurably large
|
||||
|
||||
plant-analyzer-print = Print
|
||||
plant-analyzer-printout-missing = N/A
|
||||
plant-analyzer-printout = [color=#9FED58][head=2]Plant Analyzer Report[/head][/color]{$nl
|
||||
}──────────────────────────────{$nl
|
||||
}[bullet/] Species: {$seedName}{$nl
|
||||
}{$indent}[bullet/] Viable: {$viable ->
|
||||
[no][color=red]No[/color]
|
||||
[yes][color=green]Yes[/color]
|
||||
*[other]{LOC("plant-analyzer-printout-missing")}
|
||||
}{$nl
|
||||
}{$indent}[bullet/] Endurance: {$endurance}{$nl
|
||||
}{$indent}[bullet/] Lifespan: {$lifespan}{$nl
|
||||
}{$indent}[bullet/] Product: [color=#a4885c]{$produce}[/color]{$nl
|
||||
}{$indent}[bullet/] Kudzu: {$kudzu ->
|
||||
[no][color=green]No[/color]
|
||||
[yes][color=red]Yes[/color]
|
||||
*[other]{LOC("plant-analyzer-printout-missing")}
|
||||
}{$nl
|
||||
}[bullet/] Growth profile:{$nl
|
||||
}{$indent}[bullet/] Water: [color=cyan]{$water}[/color]{$nl
|
||||
}{$indent}[bullet/] Nutrition: [color=orange]{$nutrients}[/color]{$nl
|
||||
}{$indent}[bullet/] Toxins: [color=yellowgreen]{$toxins}[/color]{$nl
|
||||
}{$indent}[bullet/] Pests: [color=magenta]{$pests}[/color]{$nl
|
||||
}{$indent}[bullet/] Weeds: [color=red]{$weeds}[/color]{$nl
|
||||
}[bullet/] Environmental profile:{$nl
|
||||
}{$indent}[bullet/] Composition: [bold]{$gasesIn}[/bold]{$nl
|
||||
}{$indent}[bullet/] Pressure: [color=lightblue]{$kpa}kPa ± {$kpaTolerance}kPa[/color]{$nl
|
||||
}{$indent}[bullet/] Temperature: [color=lightsalmon]{$temp}°k ± {$tempTolerance}°k[/color]{$nl
|
||||
}{$indent}[bullet/] Light: [color=gray][bold]{$lightLevel} ± {$lightTolerance}[/bold][/color]{$nl
|
||||
}[bullet/] Flowers: {$yield ->
|
||||
[-1]{LOC("plant-analyzer-printout-missing")}
|
||||
[0][color=red]0[/color]
|
||||
*[other][color=lightgreen]{$yield} {$potency}[/color]
|
||||
}{$nl
|
||||
}[bullet/] Seeds: {$seeds ->
|
||||
[no][color=red]No[/color]
|
||||
[yes][color=green]Yes[/color]
|
||||
*[other]{LOC("plant-analyzer-printout-missing")}
|
||||
}{$nl
|
||||
}[bullet/] Chemicals: [color=gray][bold]{$chemicals}[/bold][/color]{$nl
|
||||
}[bullet/] Emissions: [bold]{$gasesOut}[/bold]
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
plant-analyzer-component-no-seed = растение не найдено
|
||||
plant-analyzer-component-health = Здоровье:
|
||||
plant-analyzer-component-age = Возраст:
|
||||
plant-analyzer-component-water = Вода:
|
||||
plant-analyzer-component-nutrition = Пит.вещ:
|
||||
plant-analyzer-component-toxins = Токсины:
|
||||
plant-analyzer-component-pests = Вредители:
|
||||
plant-analyzer-component-weeds = Сорняки:
|
||||
plant-analyzer-component-alive = [color=green]ЖИВОЕ[color]
|
||||
plant-analyzer-component-dead = [color=red]МЕРТВОЕ[color]
|
||||
plant-analyzer-component-unviable = [color=red]ГЕН СМЕРТИ[color]
|
||||
plant-analyzer-component-mutating = [color=#00ff5f]МУТИРУЕТ[color]
|
||||
plant-analyzer-component-kudzu = [color=red]КУДЗУ[color]
|
||||
plant-analyzer-soil = В этом {$holder} содержится некоторое количество [color=white]{$chemicals}[/color], которое {$count ->
|
||||
[one]имеет
|
||||
*[other]имеют
|
||||
} not been absorbed.
|
||||
plant-analyzer-soil-empty = В этом {$holder} нет непоглощенных химических веществ.
|
||||
plant-analyzer-component-environemt = Это [color=green]{$seedName}[/color] требует атмосферы при уровне давления [color=lightblue]{$kpa}кПа ± {$kpaTolerance}кПа[/color], температуры [color=lightsalmon]{$temp}°к ± {$tempTolerance}°к[/color] и уровня освещения [color=white]{$lightLevel} ± {$lightTolerance}[/color].
|
||||
plant-analyzer-component-environemt-void = Это [color=green]{$seedName}[/color] должно выращиваться [bolditalic]в вакууме космоса[/bolditalic] при уровне освещения [color=white]{$lightLevel} ± {$lightTolerance}[/color].
|
||||
plant-analyzer-component-environemt-gas = Это [color=green]{$seedName}[/color] требует атмосферы, содержащей [bold]{$gases}[/bold] при уровне давления [color=lightblue]{$kpa}кПа ± {$kpaTolerance}кПа[/color], температуры [color=lightsalmon]{$temp}°к ± {$tempTolerance}°к[/color] и уровне освещения [color=white]{$lightLevel} ± {$lightTolerance}[/color].
|
||||
plant-analyzer-produce-plural = {MAKEPLURAL($thing)}
|
||||
plant-analyzer-output = {$yield ->
|
||||
[0]{$gasCount ->
|
||||
[0]The only thing it seems to do is consume water and nutrients.
|
||||
*[other]The only thing it seems to do is turn water and nutrients into [bold]{$gases}[/bold].
|
||||
}
|
||||
*[other]It has [color=lightgreen]{$yield} {$potency}[/color]{$seedless ->
|
||||
[true]{" "}but [color=red]seedless[/color]
|
||||
*[false]{$nothing}
|
||||
}{" "}{$yield ->
|
||||
[one]flower
|
||||
*[other]flowers
|
||||
}{" "}that{$gasCount ->
|
||||
[0]{$nothing}
|
||||
*[other]{$yield ->
|
||||
[one]{" "}emits
|
||||
*[other]{" "}emit
|
||||
}{" "}[bold]{$gases}[/bold] and
|
||||
}{" "}will turn into{$yield ->
|
||||
[one]{" "}{INDEFINITE($firstProduce)} [color=#a4885c]{$produce}[/color]
|
||||
*[other]{" "}[color=#a4885c]{$producePlural}[/color]
|
||||
}.{$chemCount ->
|
||||
[0]{$nothing}
|
||||
*[other]{" "}There are trace amounts of [color=white]{$chemicals}[/color] in its stem.
|
||||
}
|
||||
}
|
||||
plant-analyzer-potency-tiny = микроскопическое
|
||||
plant-analyzer-potency-small = маленькое
|
||||
plant-analyzer-potency-below-average = ниже среднего размера
|
||||
plant-analyzer-potency-average = среднего размера
|
||||
plant-analyzer-potency-above-average = выше среднего размера
|
||||
plant-analyzer-potency-large = довольно большое
|
||||
plant-analyzer-potency-huge = огромное
|
||||
plant-analyzer-potency-gigantic = гигантское
|
||||
plant-analyzer-potency-ludicrous = нелепо большое
|
||||
plant-analyzer-potency-immeasurable = немерено большое
|
||||
plant-analyzer-print = Печать
|
||||
plant-analyzer-printout-missing = Н/Д
|
||||
plant-analyzer-printout = [color=#9FED58][head=2]Отчет анализатора растений[/head][/color]{$nl
|
||||
}──────────────────────────────{$nl
|
||||
}[bullet/] Вид: {$seedName}{$nl
|
||||
}{$indent}[bullet/] Пригодность: {$viable ->
|
||||
[no][color=red]Нет[/color]
|
||||
[yes][color=green]Да[/color]
|
||||
*[other]{LOC("plant-analyzer-printout-missing")}
|
||||
}{$nl
|
||||
}{$indent}[bullet/] Выносливость: {$endurance}{$nl
|
||||
}{$indent}[bullet/] Продолжительность жизни: {$lifespan}{$nl
|
||||
}{$indent}[bullet/] Продукт: [color=#a4885c]{$produce}[/color]{$nl
|
||||
}{$indent}[bullet/] Кудзу: {$kudzu ->
|
||||
[no][color=green]Нет[/color]
|
||||
[yes][color=red]Да[/color]
|
||||
*[other]{LOC("plant-analyzer-printout-missing")}
|
||||
}{$nl
|
||||
}[bullet/] Профиль роста:{$nl
|
||||
}{$indent}[bullet/] Вода: [color=cyan]{$water}[/color]{$nl
|
||||
}{$indent}[bullet/] Питательные вещества: [color=orange]{$nutrients}[/color]{$nl
|
||||
}{$indent}[bullet/] Токсины: [color=yellowgreen]{$toxins}[/color]{$nl
|
||||
}{$indent}[bullet/] Вредители: [color=magenta]{$pests}[/color]{$nl
|
||||
}{$indent}[bullet/] Сорняки: [color=red]{$weeds}[/color]{$nl
|
||||
}[bullet/] Профиль окружающей среды:{$nl
|
||||
}{$indent}[bullet/] Состав: [bold]{$gasesIn}[/bold]{$nl
|
||||
}{$indent}[bullet/] Давление: [color=lightblue]{$kpa}kPa ± {$kpaTolerance}kPa[/color]{$nl
|
||||
}{$indent}[bullet/] Температура: [color=lightsalmon]{$temp}°k ± {$tempTolerance}°k[/color]{$nl
|
||||
}{$indent}[bullet/] Освещение: [color=gray][bold]{$lightLevel} ± {$lightTolerance}[/bold][/color]{$nl
|
||||
}[bullet/] Цветы: {$yield ->
|
||||
[-1]{LOC("plant-analyzer-printout-missing")}
|
||||
[0][color=red]0[/color]
|
||||
*[other][color=lightgreen]{$yield} {$potency}[/color]
|
||||
}{$nl
|
||||
}[bullet/] Семена: {$seeds ->
|
||||
[no][color=red]Нет[/color]
|
||||
[yes][color=green]Да[/color]
|
||||
*[other]{LOC("plant-analyzer-printout-missing")}
|
||||
}{$nl
|
||||
}[bullet/] Химические вещества: [color=gray][bold]{$chemicals}[/bold][/color]{$nl
|
||||
}[bullet/] Выбросы: [bold]{$gasesOut}[/bold]
|
||||
|
|
@ -108,6 +108,7 @@
|
|||
prob: 0.3
|
||||
- id: ClothingBeltPlant
|
||||
- id: PlantBag ##Some maps don't have nutrivend
|
||||
- id: PlantAnalyzer
|
||||
- id: BoxMouthSwab
|
||||
- id: Dropper
|
||||
- id: HandLabeler
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
HydroponicsToolClippers: 4
|
||||
HydroponicsToolScythe: 4
|
||||
HydroponicsToolHatchet: 4
|
||||
PlantAnalyzer: 4
|
||||
Dropper: 4
|
||||
PlantBag: 3
|
||||
PlantBGoneSpray: 20
|
||||
|
|
@ -17,8 +18,6 @@
|
|||
Bucket: 3
|
||||
BoxMouthSwab: 1
|
||||
BoxAgrichem: 1
|
||||
#TO DO:
|
||||
#plant analyzer
|
||||
contrabandInventory:
|
||||
ChemistryBottleUnstableMutagen: 1
|
||||
Joint: 1
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@
|
|||
- type: Storage
|
||||
whitelist:
|
||||
tags:
|
||||
# - PlantAnalyzer
|
||||
- PlantAnalyzer
|
||||
- PlantSampleTaker
|
||||
- BotanyShovel
|
||||
- BotanyHoe
|
||||
|
|
@ -389,10 +389,10 @@
|
|||
whitelist:
|
||||
tags:
|
||||
- BotanyHatchet
|
||||
# hydro:
|
||||
# whitelist:
|
||||
# tags:
|
||||
# - PlantAnalyzer # Dunno what to put here, should be aight.
|
||||
hydro:
|
||||
whitelist:
|
||||
tags:
|
||||
- PlantAnalyzer
|
||||
hoe:
|
||||
whitelist:
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
- type: entity
|
||||
id: PlantAnalyzer
|
||||
parent: BaseItem
|
||||
name: plant analyzer
|
||||
description: A scanner used to evaluate a plant's various areas of growth, genetic traits and chemicals.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Specific/Hydroponics/plant_analyzer.rsi
|
||||
layers:
|
||||
- state: icon
|
||||
- state: analyzer
|
||||
shader: unshaded
|
||||
- type: Item
|
||||
storedRotation: -90
|
||||
- type: Tag
|
||||
tags:
|
||||
- PlantAnalyzer
|
||||
- type: ActivatableUI
|
||||
key: enum.PlantAnalyzerUiKey.Key
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.PlantAnalyzerUiKey.Key:
|
||||
type: PlantAnalyzerBoundUserInterface
|
||||
- type: ItemToggle
|
||||
onUse: false
|
||||
- type: PlantAnalyzer
|
||||
- type: GuideHelp
|
||||
guides:
|
||||
- Botany
|
||||
- Chemicals
|
||||
|
||||
- type: entity
|
||||
name: plant analyzer report
|
||||
parent: Paper
|
||||
id: PlantAnalyzerReportPaper
|
||||
description: A printout from a plant analyzer.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Misc/bureaucracy.rsi
|
||||
layers:
|
||||
- state: paper_receipt_horizontal
|
||||
- state: paper_receipt_horizontal_words
|
||||
map: ["enum.PaperVisualLayers.Writing"]
|
||||
visible: false
|
||||
- state: paper_stamp-generic
|
||||
map: ["enum.PaperVisualLayers.Stamp"]
|
||||
visible: false
|
||||
- type: PaperVisuals
|
||||
backgroundImagePath: "/Textures/Interface/Paper/paper_background_perforated.svg.96dpi.png"
|
||||
backgroundImageTile: true
|
||||
backgroundPatchMargin: 6.0, 0.0, 6.0, 0.0
|
||||
contentMargin: 6.0, 6.0, 6.0, 6.0
|
||||
maxWritableArea: 375.0, 600.0
|
||||
|
|
@ -130,6 +130,7 @@
|
|||
- ServiceStatic
|
||||
- PowerCellsStatic
|
||||
- ElectronicsStatic
|
||||
- HydroponicsStatic
|
||||
- type: EmagLatheRecipes
|
||||
emagStaticPacks:
|
||||
- SecurityAmmoStatic
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@
|
|||
- BoozeDispenserMachineCircuitboard
|
||||
- SodaDispenserMachineCircuitboard
|
||||
|
||||
- type: latheRecipePack
|
||||
id: HydroponicsStatic
|
||||
recipes:
|
||||
- HydroponicsToolMiniHoe
|
||||
- HydroponicsToolScythe
|
||||
- HydroponicsToolHatchet
|
||||
- HydroponicsToolSpade
|
||||
- HydroponicsToolClippers
|
||||
- PlantAnalyzer
|
||||
|
||||
## Dynamic
|
||||
|
||||
- type: latheRecipePack
|
||||
|
|
|
|||
|
|
@ -37,3 +37,8 @@
|
|||
parent: BaseHydroToolRecipe
|
||||
id: HydroponicsToolClippers
|
||||
result: HydroponicsToolClippers
|
||||
|
||||
- type: latheRecipe
|
||||
parent: HandheldHealthAnalyzer
|
||||
id: PlantAnalyzer
|
||||
result: PlantAnalyzer
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 805 B |
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"version": 1,
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/f032bb1d4d982d4ee57c214c18238aaddb45d512",
|
||||
"states": [
|
||||
{
|
||||
"name": "analyzer",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "icon"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue