Анализатор растений (#1755)

Co-authored-by: Ignaz Kraft <ignaz.k@live.de>
This commit is contained in:
Vigers Ray 2025-04-12 04:05:07 +03:00 committed by GitHub
parent b4d0729d3b
commit 2c1db3ffdf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1445 additions and 256 deletions

View file

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

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

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Botany.PlantAnalyzer;
[Serializable, NetSerializable]
public enum PlantAnalyzerUiKey : byte
{
Key
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -130,6 +130,7 @@
- ServiceStatic
- PowerCellsStatic
- ElectronicsStatic
- HydroponicsStatic
- type: EmagLatheRecipes
emagStaticPacks:
- SecurityAmmoStatic

View file

@ -35,6 +35,16 @@
- BoozeDispenserMachineCircuitboard
- SodaDispenserMachineCircuitboard
- type: latheRecipePack
id: HydroponicsStatic
recipes:
- HydroponicsToolMiniHoe
- HydroponicsToolScythe
- HydroponicsToolHatchet
- HydroponicsToolSpade
- HydroponicsToolClippers
- PlantAnalyzer
## Dynamic
- type: latheRecipePack

View file

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

View file

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