Compare commits

..
423 changed files with 32716 additions and 17563 deletions
@@ -174,13 +174,7 @@ public sealed partial class GuideMicrowaveEmbed : PanelContainer, IDocumentTag,
private void GenerateCookTime(FoodRecipePrototype recipe)
{
var msg = new FormattedMessage();
// DeltaV - Deep fryer formatting
var locId = recipe.DeepFried
? "guideboook-microwave-fry-time"
: "guidebook-microwave-cook-time-deltav";
msg.AddMarkupOrThrow(Loc.GetString(locId, ("time", recipe.CookTime)));
// End DV
msg.AddMarkupOrThrow(Loc.GetString("guidebook-microwave-cook-time", ("time", recipe.CookTime)));
msg.Pop();
CookTimeLabel.SetMessage(msg);
@@ -0,0 +1,11 @@
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Client.Kitchen.Components
{
[RegisterComponent]
//Unnecessary item: [ComponentReference(typeof(SharedDeepFriedComponent))]
public sealed partial class DeepFriedComponent : SharedDeepFriedComponent
{
}
}
@@ -0,0 +1,11 @@
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Client.Kitchen.Components
{
[RegisterComponent]
// Unnecessary line: [ComponentReference(typeof(SharedDeepFryerComponent))]
public sealed partial class DeepFryerComponent : SharedDeepFryerComponent
{
}
}
@@ -0,0 +1,64 @@
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Robust.Client.GameObjects;
namespace Content.Client.Nyanotrasen.Kitchen.UI
{
public sealed class DeepFryerBoundUserInterface : BoundUserInterface
{
private DeepFryerWindow? _window;
private NetEntity[] _entities = default!;
public DeepFryerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
base.Open();
_window = new DeepFryerWindow();
_window.OnClose += Close;
_window.ItemList.OnItemSelected += args =>
{
SendMessage(new DeepFryerRemoveItemMessage(_entities[args.ItemIndex]));
};
_window.InsertItem.OnPressed += _ =>
{
SendMessage(new DeepFryerInsertItemMessage());
};
_window.ScoopVat.OnPressed += _ =>
{
SendMessage(new DeepFryerScoopVatMessage());
};
_window.ClearSlag.OnPressed += args =>
{
SendMessage(new DeepFryerClearSlagMessage());
};
_window.RemoveAllItems.OnPressed += _ =>
{
SendMessage(new DeepFryerRemoveAllItemsMessage());
};
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null)
return;
if (state is not DeepFryerBoundUserInterfaceState cast)
return;
_entities = cast.ContainedEntities;
_window.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_window?.Dispose();
}
}
}
@@ -0,0 +1,67 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'deep-fryer-window-title'}"
MinSize="600 400"
>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<BoxContainer
Orientation="Vertical"
HorizontalExpand="True"
>
<Label
Text="{Loc 'deep-fryer-label-baskets'}"
Align="Left"/>
<ItemList Name="ItemList"
Access="Public"
VerticalExpand="True"
HorizontalExpand="True"
SelectMode="Button">
</ItemList>
</BoxContainer>
<BoxContainer
Orientation="Vertical"
Margin="8 0"
MinSize="200 0"
>
<Label Text="{Loc 'deep-fryer-label-oil-level'}"/>
<ProgressBar Name="OilLevel"
HorizontalExpand="True"
MinValue="0"
MaxValue="1"
Page="0"
Value="1">
</ProgressBar>
<Label Text="{Loc 'deep-fryer-label-oil-purity'}"/>
<ProgressBar Name="OilPurity"
HorizontalExpand="True"
MinValue="0"
MaxValue="1"
Page="0"
Value="1">
</ProgressBar>
<Button Name="InsertItem"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-insert-item'}"
ToolTip="{Loc 'deep-fryer-button-insert-item-tooltip'}"/>
<Button Name="ScoopVat"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-scoop-vat'}"
ToolTip="{Loc 'deep-fryer-button-scoop-vat-tooltip'}"/>
<Button Name="ClearSlag"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-clear-slag'}"
ToolTip="{Loc 'deep-fryer-button-clear-slag-tooltip'}"/>
<Button Name="RemoveAllItems"
Access="Public"
TextAlign="Center"
HorizontalExpand="True"
Text="{Loc 'deep-fryer-button-remove-all-items'}"
ToolTip="{Loc 'deep-fryer-button-remove-all-items-tooltip'}"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>
@@ -0,0 +1,72 @@
using System.Numerics;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Nyanotrasen.Kitchen.UI
{
[GenerateTypedNameReferences]
[Access(typeof(DeepFryerBoundUserInterface))]
public sealed partial class DeepFryerWindow : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private static readonly Color WarningColor = Color.FromHsv(new Vector4(0.0f, 1.0f, 0.8f, 1.0f));
public DeepFryerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void UpdateState(DeepFryerBoundUserInterfaceState state)
{
OilLevel.Value = (float) state.OilLevel;
OilPurity.Value = (float) state.OilPurity;
if (state.OilPurity < state.FryingOilThreshold)
{
if (OilPurity.ForegroundStyleBoxOverride == null)
{
OilPurity.ForegroundStyleBoxOverride = new StyleBoxFlat();
var oilPurityStyle = (StyleBoxFlat) OilPurity.ForegroundStyleBoxOverride;
oilPurityStyle.BackgroundColor = WarningColor;
}
}
else
{
OilPurity.ForegroundStyleBoxOverride = null;
}
ItemList.Clear();
foreach (var netEntity in state.ContainedEntities)
{
var entity = _entityManager.GetEntity(netEntity);
if (_entityManager.Deleted(entity))
continue;
// Duplicated from MicrowaveBoundUserInterface.cs: keep an eye on that file for when it changes.
Texture? texture;
if (_entityManager.TryGetComponent(entity, out IconComponent? iconComponent))
{
texture = _entityManager.System<SpriteSystem>().GetIcon(iconComponent);
}
else if (_entityManager.TryGetComponent(entity, out SpriteComponent? spriteComponent))
{
texture = spriteComponent.Icon?.Default;
}
else
{
continue;
}
ItemList.AddItem(_entityManager.GetComponent<MetaDataComponent>(entity).EntityName, texture);
}
}
}
}
@@ -0,0 +1,75 @@
using System.Linq;
using Robust.Client.GameObjects;
using static Robust.Client.GameObjects.SpriteComponent;
using Content.Client.Kitchen.Components;
using Content.Shared.Clothing;
using Content.Shared.Hands;
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Client.Kitchen.Visualizers;
public sealed class DeepFriedVisualizerSystem : VisualizerSystem<DeepFriedComponent>
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private readonly static string ShaderName = "Crispy";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeepFriedComponent, HeldVisualsUpdatedEvent>(OnHeldVisualsUpdated);
SubscribeLocalEvent<DeepFriedComponent, EquipmentVisualsUpdatedEvent>(OnEquipmentVisualsUpdated);
}
protected override void OnAppearanceChange(EntityUid uid, DeepFriedComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!_appearance.TryGetData(uid, DeepFriedVisuals.Fried, out bool isFried, args.Component))
return;
for (var i = 0; i < args.Sprite.AllLayers.Count(); ++i)
args.Sprite.LayerSetShader(i, ShaderName);
}
private void OnHeldVisualsUpdated(EntityUid uid, DeepFriedComponent component, HeldVisualsUpdatedEvent args)
{
if (args.RevealedLayers.Count == 0)
{
return;
}
if (!TryComp(args.User, out SpriteComponent? sprite))
return;
foreach (var key in args.RevealedLayers)
{
if (!sprite.LayerMapTryGet(key, out var index) || sprite[index] is not Layer layer)
continue;
sprite.LayerSetShader(index, ShaderName);
}
}
private void OnEquipmentVisualsUpdated(EntityUid uid, DeepFriedComponent component, EquipmentVisualsUpdatedEvent args)
{
if (args.RevealedLayers.Count == 0)
{
return;
}
if (!TryComp(args.Equipee, out SpriteComponent? sprite))
return;
foreach (var key in args.RevealedLayers)
{
if (!sprite.LayerMapTryGet(key, out var index) || sprite[index] is not Layer layer)
continue;
sprite.LayerSetShader(index, ShaderName);
}
}
}
@@ -1,8 +1,11 @@
using Content.Shared._DV.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Robust.Client.GameObjects;
using Content.Client.Chemistry.Visualizers;
using Content.Client.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Client._DV.Kitchen;
namespace Content.Client.Kitchen.Visualizers;
public sealed class DeepFryerVisualizerSystem : VisualizerSystem<DeepFryerComponent>
{
@@ -19,3 +22,4 @@ public sealed class DeepFryerVisualizerSystem : VisualizerSystem<DeepFryerCompon
scvComponent.FillBaseName = isBubbling ? "on-" : "off-";
}
}
@@ -1,86 +0,0 @@
using Content.Client.Stylesheets;
using Content.Client.Stylesheets.Sheetlets;
using Content.Client.Stylesheets.SheetletConfigs;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client._DV.AACTablet.UI;
// ReSharper disable InconsistentNaming
[CommonSheetlet]
public sealed class AACSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, IButtonConfig
{
public override StyleRule[] GetRules(T sheet, object config)
{
// Create style boxes
var controlBarBox = new StyleBoxFlat { BackgroundColor = Color.FromHex("#2a2a35") };
var contentAreaBox = new StyleBoxFlat { BackgroundColor = Color.FromHex("#25252a") };
var bufferDisplayBox = new StyleBoxFlat { BackgroundColor = Color.FromHex("#1a1a20") };
bufferDisplayBox.SetContentMarginOverride(StyleBox.Margin.Horizontal, 8);
bufferDisplayBox.SetContentMarginOverride(StyleBox.Margin.Vertical, 4);
var searchBarBox = new StyleBoxFlat { BackgroundColor = Color.FromHex("#2a2a35") };
var transparentInputBox = new StyleBoxFlat
{
BackgroundColor = Color.Transparent,
ContentMarginLeftOverride = 4,
ContentMarginRightOverride = 4
};
var rules = new List<StyleRule>
{
// AAC - Control Bar Background
E<PanelContainer>()
.Class("AacControlBar")
.Panel(controlBarBox),
// AAC - Content Area Background
E<PanelContainer>()
.Class("AacContentArea")
.Panel(contentAreaBox),
// AAC - Buffer Display (where combined phrases show)
E<PanelContainer>()
.Class("AacBufferDisplay")
.Panel(bufferDisplayBox),
// AAC - Search Bar
E<PanelContainer>()
.Class("AacSearchBar")
.Panel(searchBarBox),
// AAC - Buffer Text Style
E<LineEdit>()
.Class("AacBufferText")
.Prop(LineEdit.StylePropertyStyleBox, transparentInputBox),
// AAC - Search Input Style
E<LineEdit>()
.Class("AacSearchInput")
.Prop(LineEdit.StylePropertyStyleBox, transparentInputBox),
// AAC - Footer Text
E<Label>()
.Class("AacFooterText")
.Font(sheet.BaseFont.GetFont(10))
.FontColor(Color.FromHex("#757575")),
};
// Set the base button stylebox for AAC buttons
rules.AddRange([
E<ContainerButton>()
.Class(ContainerButton.StyleClassButton)
.Class("SendButton")
.Box(StyleBoxHelpers.BaseStyleBox(sheet)),
E<ContainerButton>()
.Class(ContainerButton.StyleClassButton)
.Class("ClearButton")
.Prop(Control.StylePropertyModulateSelf, Color.DarkRed),
]);
return rules.ToArray();
}
}
+14 -89
View File
@@ -2,97 +2,22 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'aac-tablet-title'}"
Resizable="True"
SetSize="580 422"
MinSize="580 320">
SetSize="540 300"
MinSize="540 300">
<BoxContainer Orientation="Vertical">
<!-- Main Content Area -->
<BoxContainer Name="MainContent" Orientation="Vertical" VerticalExpand="True" Margin="8 8 8 4">
<!-- Top Control Bar -->
<PanelContainer StyleClasses="AacControlBar" Margin="0 0 0 8">
<BoxContainer Orientation="Vertical" Margin="8">
<!-- Buffer Toggle Row -->
<BoxContainer Orientation="Horizontal" Margin="0 0 0 6">
<CheckBox Name="ShouldBuffer"
Text="{Loc 'aac-tablet-combine'}"
StyleClasses="AacCheckbox"
Margin="0 0 8 0"/>
<Control HorizontalExpand="True"/>
</BoxContainer>
<!-- Buffer Display and Controls -->
<BoxContainer Orientation="Horizontal">
<PanelContainer HorizontalExpand="True" StyleClasses="AacBufferDisplay">
<LineEdit Name="BufferedString"
Editable="False"
HorizontalExpand="True"
StyleClasses="AacBufferText"
Margin="8 4"/>
</PanelContainer>
<Button Name="ClearButton"
Text="{Loc 'aac-tablet-backspace'}"
TextAlign="Center"
StyleClasses="ClearButton"
SetWidth="90"
Margin="8 0 0 0"/>
<Button Name="SendButton"
Text="{Loc 'aac-tablet-send'}"
TextAlign="Center"
StyleClasses="SendButton"
SetWidth="90"
Margin="8 0 0 0"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>
<!-- Tabbed Content Area -->
<PanelContainer VerticalExpand="True" StyleClasses="AacContentArea">
<TabContainer Name="WindowBody"
TabTitle="Search"
VerticalExpand="True"
MouseFilter="Pass"
Margin="4">
<!-- Search Tab -->
<BoxContainer Orientation="Vertical" Name="Search" VerticalExpand="True">
<!-- Search Bar -->
<PanelContainer StyleClasses="AacSearchBar" Margin="4 4 4 8">
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'aac-tablet-search-placeholder'}"
HorizontalExpand="True"
StyleClasses="AacSearchInput"
Margin="8 6"/>
</PanelContainer>
<!-- Search Results -->
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
HScrollEnabled="False"
Margin="0 -5">
<BoxContainer Name="SearchResults"
Orientation="Vertical"
HorizontalExpand="True"
Margin="0 -5"/>
</ScrollContainer>
</BoxContainer>
</TabContainer>
</PanelContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<CheckBox Name="ShouldBuffer" Text="{Loc 'aac-tablet-combine'}"/>
<LineEdit Name="BufferedString" Editable="False" HorizontalExpand="True"/>
<Button Name="ClearButton" Text="{Loc 'aac-tablet-backspace'}" TextAlign="Center"/>
<Button Name="SendButton" Text="{Loc 'aac-tablet-send'}" TextAlign="Center"/>
</BoxContainer>
<!-- Footer -->
<BoxContainer Name="Footer" Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider"/>
<BoxContainer Orientation="Horizontal" Margin="10 4 10 4" VerticalAlignment="Center">
<Label Text="{Loc 'aac-tablet-footer-hint'}"
StyleClasses="AacFooterText"
VerticalAlignment="Center"/>
<Control HorizontalExpand="True"/>
<Label Text="{Loc 'aac-tablet-footer-info'}"
StyleClasses="AacFooterText"
VerticalAlignment="Center"
Margin="0 0 8 0"/>
<TabContainer Name="WindowBody" TabTitle="Search" VerticalExpand="True" MouseFilter="Pass">
<BoxContainer Orientation="Vertical" Name="Search" MinSize="540 200">
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True" Margin="4 4" />
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="SearchResults" Orientation="Vertical" HorizontalExpand="True" />
</ScrollContainer>
</BoxContainer>
</BoxContainer>
</TabContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -1,32 +0,0 @@
using Content.Client.Administration.Systems;
using Robust.Shared.Console;
namespace Content.Client._DV.Administration.Commands;
public sealed class AdminOverlayCommand : LocalizedEntityCommands
{
[Dependency] private readonly AdminSystem _admin = default!;
public override string Command => "adminoverlay";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1 || !Boolean.TryParse(args[0], out var newValue))
{
shell.WriteLine(Help);
return;
}
if (newValue)
{
_admin.AdminOverlayOn();
}
else
{
_admin.AdminOverlayOff();
}
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length == 1 ? CompletionResult.FromOptions(CompletionHelper.Booleans) : CompletionResult.Empty;
}
}
@@ -76,8 +76,9 @@ public sealed partial class CosmicCultSystem : SharedCosmicCultSystem
if (args.Alert.ID != ent.Comp.EntropyAlert)
return;
var entropy = Math.Clamp(ent.Comp.EntropyStored, 0, 14);
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), AlertVisualLayers.Base, $"base{entropy}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), CultAlertVisualLayers.Counter, $"num{entropy}");
var sprite = args.SpriteViewEnt.Comp;
_sprite.LayerSetRsiState((ent, sprite), AlertVisualLayers.Base, $"base{entropy}");
_sprite.LayerSetRsiState((ent, sprite), CultAlertVisualLayers.Counter, $"num{entropy}");
}
#endregion
@@ -1,5 +0,0 @@
using Content.Shared._DV.Kitchen.Systems;
namespace Content.Client._DV.Kitchen;
public sealed class DeepFryerSystem : SharedDeepFryerSystem;
@@ -1,43 +0,0 @@
using Content.Client.Stylesheets;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client._DV.Stylesheets.Sheetlets;
[CommonSheetlet]
public sealed class SurgeryTextureButtonSheetlet <T> : Sheetlet<T> where T : PalettedStylesheet
{
private readonly Color _dollColor = Color.FromHex("#639bff");
public override StyleRule[] GetRules(T sheet, object config)
{
return
[
E<TextureButton>()
.Identifier("SurgeryTextureButton")
.PseudoHovered()
.Modulate(sheet.PositivePalette.HoveredElement),
E<TextureButton>()
.Identifier("SurgeryTextureButton")
.PseudoPressed()
.Modulate(sheet.PositivePalette.PressedElement),
E<TextureButton>()
.Identifier("SurgeryTextureButton")
.PseudoNormal()
.Modulate(_dollColor),
E<TextureButton>()
.Identifier("OpenIncision")
.PseudoHovered()
.Modulate(sheet.PositivePalette.HoveredElement),
E<TextureButton>()
.Identifier("OpenIncision")
.PseudoPressed()
.Modulate(sheet.PositivePalette.PressedElement),
E<TextureButton>()
.Identifier("OpenIncision")
.PseudoNormal()
.Modulate(sheet.HighlightPalette.HoveredElement),
];
}
}
@@ -1,14 +1,11 @@
using Content.Client._Shitmed.Choice.UI;
using Content.Client.Administration.UI.CustomControls;
using Content.Shared._Shitmed.Medical.Surgery;
using Content.Shared._Shitmed.Medical.Surgery.Steps.Parts; // DeltaV - New UI
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Humanoid;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls; // DeltaV - New UI
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -27,7 +24,6 @@ public sealed class SurgeryBui : BoundUserInterface
private bool _isBody;
private (EntityUid Ent, EntProtoId Proto)? _surgery;
private readonly List<EntProtoId> _previousSurgeries = new();
private (EntityUid Ent, TextureButton)? _previousPart; // DeltaV - New UI
public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) => _system = _entities.System<SurgerySystem>();
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
@@ -68,7 +64,6 @@ public sealed class SurgeryBui : BoundUserInterface
_isBody = false;
_surgery = null;
_previousSurgeries.Clear();
DeactivateOtherParts(); // DeltaV - New UI
View(ViewType.Parts);
};
@@ -143,37 +138,16 @@ public sealed class SurgeryBui : BoundUserInterface
return GetScore(a.PartType) - GetScore(b.PartType);
});
// DeltaV Start - New UI
foreach (var textureButton in _window.Buttons)
{
textureButton.Visible = false;
textureButton.StyleIdentifier = "SurgeryTextureButton";
}
// DeltaV End - New UI
foreach (var (netEntity, entity, partName, bodyPartType) in options) // DeltaV - New UI
foreach (var (netEntity, entity, partName, _) in options)
{
//var netPart = _entities.GetNetEntity(part.Owner);
var surgeries = state.Choices[netEntity];
var partButton = new ChoiceControl();
// Delta Start - New UI
var button = GetBodyPartTextureButton(entity, bodyPartType);
if (button != null)
{
button.Visible = true;
button.OnPressed += _ => OnPartPressed(netEntity, surgeries);
if (_entities.HasComponent<IncisionOpenComponent>(entity))
button.StyleIdentifier = "OpenIncision";
}
else
{
var partButton = new ChoiceControl();
partButton.Set(partName, null);
partButton.Button.OnPressed += _ => OnPartPressed(netEntity, surgeries);
partButton.Set(partName, null);
partButton.Button.OnPressed += _ => OnPartPressed(netEntity, surgeries);
_window.Parts.AddChild(partButton);
}
// Delta End - New UI
_window.Parts.AddChild(partButton);
foreach (var surgeryId in surgeries)
{
@@ -253,35 +227,7 @@ public sealed class SurgeryBui : BoundUserInterface
_part = _entities.GetEntity(netPart);
_isBody = _entities.HasComponent<BodyComponent>(_part);
// DeltaV Start - New UI
var bodyPart = _entities.GetComponent<BodyPartComponent>(_part.Value);
var body = bodyPart.Body!.Value;
if (_previousPart != null)
{
(var previousEnt, var previousButton) = _previousPart.Value;
if (!_entities.HasComponent<IncisionOpenComponent>(previousEnt))
previousButton.StyleIdentifier = "SurgeryTextureButton";
}
var button = GetBodyPartTextureButton(_part.Value, bodyPart.PartType);
if (button != null && button.Pressed)
{
_previousPart = (_part.Value, button);
DeactivateOtherParts(button);
}
else if (button != null && !button.Pressed)
{
_part = null;
_isBody = false;
_surgery = null;
_previousSurgeries.Clear();
View(ViewType.Parts);
return;
}
// DeltaV End - New UI
var body = _entities.GetComponent<BodyPartComponent>(_part.Value).Body!.Value; // DeltaV
_window.Surgeries.RemoveAllChildren();
var surgeries = new List<(Entity<SurgeryComponent> Ent, EntProtoId Id, string Name)>();
@@ -331,58 +277,6 @@ public sealed class SurgeryBui : BoundUserInterface
View(ViewType.Surgeries);
}
// DeltaV Start - New UI
private TextureButton? GetBodyPartTextureButton(EntityUid entity, BodyPartType? partType)
{
if (_window == null
|| !_entities.TryGetComponent(entity, out BodyPartComponent? bodyPart))
return null;
var isLeftPart = bodyPart.Symmetry.HasFlag(BodyPartSymmetry.Left);
switch (partType)
{
case BodyPartType.Torso:
var isHumanoid = _entities.HasComponent<HumanoidAppearanceComponent>(bodyPart.Body);
return isHumanoid ? _window.ChestButton : _window.CarpButton;
case BodyPartType.Head:
return _window.HeadButton;
case BodyPartType.Arm:
return isLeftPart ? _window.LArmButton : _window.RArmButton;
case BodyPartType.Hand:
return isLeftPart ? _window.LHandButton : _window.RHandButton;
case BodyPartType.Leg:
return isLeftPart ? _window.LLegButton : _window.RLegButton;
case BodyPartType.Foot:
return isLeftPart ? _window.LFootButton : _window.RFootButton;
case BodyPartType.Tail:
case BodyPartType.Other:
case null:
return null;
default:
throw new ArgumentOutOfRangeException(nameof(partType), partType, null);
}
}
/// <summary>
/// Deactivates all other buttons that are currently active.
/// </summary>
/// <param name="activatedButton">The button that should be the only active button.</param>
private void DeactivateOtherParts(TextureButton? activatedButton = null)
{
if (_window == null)
return;
foreach (var button in _window.Buttons)
{
if (activatedButton == button)
continue;
button.Pressed = false;
}
}
// DeltaV End - New UI
private void RefreshUI()
{
if (_window == null
@@ -1,80 +1,23 @@
<controls:FancyWindow
<controls:SurgeryWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:customcontrols="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client._Shitmed.Medical.Surgery"
xmlns:controls="clr-namespace:Content.Client._Shitmed.Medical.Surgery"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinSize="400 400">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True" MinSize="570, 336">
<BoxContainer Orientation="Vertical" HorizontalExpand="False" VerticalExpand="False" MinSize="264, 408">
<TextureRect Name="Texture" TextureScale="6 6"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUIBackground.png" Access="Public"
HorizontalAlignment="Center" VerticalAlignment="Center"
Stretch="KeepAspectCentered" StyleClasses="">
<PanelContainer>
<TextureButton
Name="CarpButton" Scale="8 8" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 11"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUICarp.png" Access="Public"/>
<TextureButton
Name="ChestButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 11"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUITorso.png" Access="Public"/>
<TextureButton
Name="HeadButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0 30 0 0"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUIHead.png" Access="Public"/>
<TextureButton
Name="RArmButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Center" HorizontalAlignment="Left" Margin="30 0 0 47"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUIRArm.png" Access="Public"/>
<TextureButton
Name="RHandButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Center" HorizontalAlignment="Left" Margin="30 61 0 0"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUIRHand.png" Access="Public"/>
<TextureButton
Name="RLegButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="78 0 0 42"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUIRLeg.png" Access="Public"/>
<TextureButton
Name="RFootButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="54 0 0 30"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUIRFoot.png" Access="Public"/>
<TextureButton
Name="LArmButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 30 47"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUILArm.png" Access="Public"/>
<TextureButton
Name="LHandButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 61 30 0"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUILHand.png" Access="Public"/>
<TextureButton
Name="LLegButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="0 0 78 42"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUILLeg.png" Access="Public"/>
<TextureButton
Name="LFootButton" Scale="6 6" ToggleMode="True" StyleIdentifier="SurgeryTextureButton" Visible="False"
VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="0 0 54 30"
TexturePath="/Textures/_Shitmed/Interface/SurgeryUI/surgeryUILFoot.png" Access="Public"/>
</PanelContainer>
</TextureRect>
</BoxContainer>
<customcontrols:VSeparator StyleClasses="LowDivider" />
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 10">
<Button Name="PartsButton" Access="Public" Text="{Loc 'surgery-ui-window-parts'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
<Button Name="SurgeriesButton" Access="Public" Text="{Loc 'surgery-ui-window-surgeries'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
<Button Name="StepsButton" Access="Public" Text="{Loc 'surgery-ui-window-steps'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
</BoxContainer>
<customcontrols:HSeparator />
<ScrollContainer VScrollEnabled="True" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="Parts" Access="Public" Orientation="Vertical" Visible="False" />
<BoxContainer Name="Surgeries" Access="Public" Orientation="Vertical" Visible="False" />
<BoxContainer Name="Steps" Access="Public" Orientation="Vertical" Visible="False" />
</ScrollContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 10">
<Button Name="PartsButton" Access="Public" Text="{Loc 'surgery-ui-window-parts'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
<Button Name="SurgeriesButton" Access="Public" Text="{Loc 'surgery-ui-window-surgeries'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
<Button Name="StepsButton" Access="Public" Text="{Loc 'surgery-ui-window-steps'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
</BoxContainer>
<cc:HSeparator />
<ScrollContainer VScrollEnabled="True" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="Parts" Access="Public" Orientation="Vertical" Visible="False" />
<BoxContainer Name="Surgeries" Access="Public" Orientation="Vertical" Visible="False" />
<BoxContainer Name="Steps" Access="Public" Orientation="Vertical" Visible="False" />
</ScrollContainer>
</BoxContainer>
</controls:FancyWindow>
</controls:SurgeryWindow>
@@ -1,30 +1,14 @@
using Content.Client.UserInterface.Controls; // DeltaV - New UI
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls; // DeltaV - New UI
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Shitmed.Medical.Surgery;
[GenerateTypedNameReferences]
public sealed partial class SurgeryWindow : FancyWindow
public sealed partial class SurgeryWindow : DefaultWindow
{
public List<TextureButton> Buttons { get; } = []; // DeltaV - New UI
public SurgeryWindow()
{
RobustXamlLoader.Load(this);
// DeltaV Start - New UI
Buttons.Add(ChestButton);
Buttons.Add(HeadButton);
Buttons.Add(LArmButton);
Buttons.Add(LHandButton);
Buttons.Add(RArmButton);
Buttons.Add(RHandButton);
Buttons.Add(LLegButton);
Buttons.Add(LFootButton);
Buttons.Add(RLegButton);
Buttons.Add(RFootButton);
Buttons.Add(CarpButton);
// DeltaV End - New UI
}
}
@@ -24,7 +24,7 @@ public sealed class WizdenContentFreeze
var protoMan = server.ProtoMan;
var recipesCount = protoMan.Count<FoodRecipePrototype>();
var recipesLimit = 269; // DeltaV - was 218
var recipesLimit = 268; // DeltaV - was 218
if (recipesCount > recipesLimit)
{
@@ -1,5 +1,4 @@
using Content.Server._DV.CosmicCult.Components; // DeltaV
using Content.Server._DV.GameTicking.Rules.Components; // DeltaV
using Content.Server.Administration.Commands;
using Content.Server.Antag;
using Content.Server.GameTicking;
@@ -210,21 +209,6 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", cosmicCultName, Loc.GetString("admin-verb-make-cosmiccultist")),
};
args.Verbs.Add(cosmiccult);
var ntAgent = Loc.GetString("admin-verb-make-NTAgent");
Verb agent = new()
{
Text = ntAgent,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/job_icons.rsi"), "Nanotrasen"),
Act = () =>
{
_antag.ForceMakeAntag<NTAgentRuleComponent>(targetPlayer, "NTAgent");
},
Impact = LogImpact.High,
Message = string.Join(": ", ntAgent, Loc.GetString("admin-verb-text-make-NTAgent")),
};
args.Verbs.Add(agent);
// End DeltaV Additions
}
}
+3 -8
View File
@@ -1,4 +1,3 @@
using Content.Server._DV.BibleUser; // DeltaV
using Content.Server.Bible.Components;
using Content.Server.Ghost.Roles.Events;
using Content.Server.Popups;
@@ -33,7 +32,6 @@ namespace Content.Server.Bible
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly BibleUserSystem _bibleUser = default!; // DeltaV - Shared Bible Cooldown
public override void Initialize()
{
@@ -118,7 +116,7 @@ namespace Content.Server.Bible
return;
}
if (!TryComp<BibleUserComponent>(args.User, out var bibleUser)) // DeltaV - Changed from HasComp to TryComp
if (!HasComp<BibleUserComponent>(args.User))
{
_popupSystem.PopupEntity(Loc.GetString("bible-sizzle"), args.User, args.User);
@@ -130,11 +128,9 @@ namespace Content.Server.Bible
}
// This only has a chance to fail if the target is not wearing anything on their head and is not a familiar.
// DeltaV - also fails if the BibleUser has used another bible in the last 10 seconds.
if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) && !HasComp<FamiliarComponent>(args.Target.Value)
|| !bibleUser.CanHeal) // DeltaV - also fails if the BibleUser has used another bible in the last 10 seconds.
if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) && !HasComp<FamiliarComponent>(args.Target.Value))
{
if (_random.Prob(component.FailChance) || !bibleUser.CanHeal) // DeltaV
if (_random.Prob(component.FailChance))
{
var othersFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(othersFailMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.SmallCaution);
@@ -166,7 +162,6 @@ namespace Content.Server.Bible
_popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large);
_audio.PlayPvs(component.HealSoundPath, args.User);
_delay.TryResetDelay((uid, useDelay));
_bibleUser.StartBibleCooldown((args.User, bibleUser)); // DeltaV - Shared bible cooldown - Only put on cooldown if the bible healed
}
}
@@ -1,27 +1,5 @@
namespace Content.Server.Bible.Components
{
[RegisterComponent]
public sealed partial class BibleUserComponent : Component
{
/// <summary>
/// DeltaV - Wheter or not the BibleUser can heal. This is to prevent multiple bibles from
/// being spammed from a single bible user.
/// </summary>
[DataField]
public bool CanHeal = true;
/// <summary>
/// DeltaV - The default cooldown of the bible user's ability to heal with a bible.
///
/// Should match UseDelay of Bible in Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
/// </summary>
[DataField]
public TimeSpan Cooldown = TimeSpan.FromSeconds(10);
/// <summary>
/// DeltaV - The time that <see cref="CanHeal"/> will be set to true again.
/// </summary>
[DataField]
public TimeSpan? NextUse;
}
public sealed partial class BibleUserComponent : Component {}
}
@@ -78,7 +78,6 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
Entry("rofl", "chatsan-laughs"),
Entry("o7", "chatsan-salutes"),
Entry(";_;7", "chatsan-tearfully-salutes"),
Entry("idk", "chatsan-shrugs"), //Delta V
Entry(";)", "chatsan-winks"),
Entry(";]", "chatsan-winks"),
Entry("(;", "chatsan-winks"),
@@ -169,7 +169,7 @@ namespace Content.Server.Abilities.Psionics
psionic.PsionicAbility = component.MindSwapReturnActionEntity;
}
public void Swap(EntityUid performer, EntityUid target, bool end = false, int ReturnSwapCooldown = 0)
public void Swap(EntityUid performer, EntityUid target, bool end = false)
{
if (end && (!HasComp<MindSwappedComponent>(performer) || !HasComp<MindSwappedComponent>(target)))
return;
@@ -211,15 +211,6 @@ namespace Content.Server.Abilities.Psionics
var perfComp = EnsureComp<MindSwappedComponent>(performer);
var targetComp = EnsureComp<MindSwappedComponent>(target);
// Delta V - Cooldown for Returning back
if (ReturnSwapCooldown > 0)
{
var cooldown = TimeSpan.FromSeconds(ReturnSwapCooldown);
_actions.SetCooldown(perfComp.MindSwapReturnActionEntity, cooldown);
_actions.SetCooldown(targetComp.MindSwapReturnActionEntity, cooldown);
}
// Delta V
perfComp.OriginalEntity = target;
targetComp.OriginalEntity = performer;
}
@@ -0,0 +1,24 @@
using Content.Shared.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
namespace Content.Server.Kitchen.Components
{
[RegisterComponent]
//This line appears to be deprecated. [ComponentReference(typeof(SharedDeepFriedComponent))]
public sealed partial class DeepFriedComponent : SharedDeepFriedComponent
{
/// <summary>
/// What is the item's base price multiplied by?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("priceCoefficient")]
public float PriceCoefficient { get; set; } = 1.0f;
/// <summary>
/// What was the entity's original name before any modification?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("originalName")]
public string? OriginalName { get; set; }
}
}
@@ -0,0 +1,191 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Construction.Prototypes;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition;
using Content.Shared.Nyanotrasen.Kitchen;
using Content.Shared.Nyanotrasen.Kitchen.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Nyanotrasen.Kitchen.Components;
// TODO: move to shared and get rid of SharedDeepFryerComponent
[RegisterComponent, Access(typeof(SharedDeepfryerSystem))]
public sealed partial class DeepFryerComponent : SharedDeepFryerComponent
{
// There are three levels to how the deep fryer treats entities.
//
// 1. An entity can be rejected by the blacklist and be untouched by
// anything other than heat damage.
//
// 2. An entity can be deep-fried but not turned into an edible. The
// change will be mostly cosmetic. Any entity that does not match
// the blacklist will fall into this category.
//
// 3. An entity can be deep-fried and turned into something edible. The
// change will permit the item to be permanently destroyed by eating
// it.
/// <summary>
/// When will the deep fryer layer on the next stage of crispiness?
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextFryTime;
/// <summary>
/// How much waste needs to be added at the next update interval?
/// </summary>
[DataField]
public FixedPoint2 WasteToAdd = FixedPoint2.Zero;
/// <summary>
/// How often are items in the deep fryer fried?
/// </summary>
[DataField]
public TimeSpan FryInterval = TimeSpan.FromSeconds(5);
/// <summary>
/// What entities cannot be deep-fried no matter what?
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// What entities can be deep-fried into being edible?
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// What are over-cooked and burned entities turned into?
/// </summary>
/// <remarks>
/// To prevent unwanted destruction of items, only food can be turned
/// into this.
/// </remarks>
[DataField]
public EntProtoId? CharredPrototype;
/// <summary>
/// What reagents are considered valid cooking oils?
/// </summary>
[DataField]
public HashSet<ProtoId<ReagentPrototype>> FryingOils = new();
/// <summary>
/// What reagents are added to tasty deep-fried food?
/// </summary>
[DataField]
public List<ReagentQuantity> GoodReagents = new();
/// <summary>
/// What reagents are added to terrible deep-fried food?
/// </summary>
[DataField]
public List<ReagentQuantity> BadReagents = new();
/// <summary>
/// What reagents replace every 1 unit of oil spent on frying?
/// </summary>
[DataField]
public List<ReagentQuantity> WasteReagents = new();
/// <summary>
/// What flavors go well with deep frying?
/// </summary>
[DataField]
public HashSet<ProtoId<FlavorPrototype>> GoodFlavors = new();
/// <summary>
/// What flavors don't go well with deep frying?
/// </summary>
[DataField]
public HashSet<ProtoId<FlavorPrototype>> BadFlavors = new();
/// <summary>
/// How much is the price coefficiency of a food changed for each good flavor?
/// </summary>
[DataField]
public float GoodFlavorPriceBonus = 0.2f;
/// <summary>
/// How much is the price coefficiency of a food changed for each bad flavor?
/// </summary>
[DataField]
public float BadFlavorPriceMalus = -0.3f;
/// <summary>
/// What is the name of the solution container for the fryer's oil?
/// </summary>
[DataField]
public string SolutionName = "vat_oil";
// TODO: Entity<SolutionComponent>
public Solution Solution = default!;
/// <summary>
/// What is the name of the entity container for items inside the deep fryer?
/// </summary>
[DataField("storage")]
public string StorageName = "vat_entities";
public BaseContainer Storage = default!;
/// <summary>
/// How much solution should be imparted based on an item's size?
/// </summary>
[DataField]
public FixedPoint2 SolutionSizeCoefficient = 1f;
/// <summary>
/// What's the maximum amount of solution that should ever be imparted?
/// </summary>
[DataField]
public FixedPoint2 SolutionSplitMax = 10f;
/// <summary>
/// What percent of the fryer's solution has to be oil in order for it to fry?
/// </summary>
/// <remarks>
/// The chef will have to clean it out occasionally, and if too much
/// non-oil reagents are added, the vat will have to be drained.
/// </remarks>
[DataField]
public FixedPoint2 FryingOilThreshold = 0.5f;
/// <summary>
/// What is the bare minimum number of oil units to prevent the fryer
/// from unsafe operation?
/// </summary>
[DataField]
public FixedPoint2 SafeOilVolume = 10f;
/// <summary>
/// What is the temperature of the vat when the deep fryer is powered?
/// </summary>
[DataField]
public float PoweredTemperature = 550.0f;
/// <summary>
/// How many entities can this deep fryer hold?
/// </summary>
[DataField]
public int StorageMaxEntities = 4;
/// <summary>
/// What sound is played when an item is inserted into hot oil?
/// </summary>
[DataField]
public SoundSpecifier SoundInsertItem = new SoundPathSpecifier("/Audio/Nyanotrasen/Machines/deepfryer_basket_add_item.ogg");
/// <summary>
/// What sound is played when an item is removed?
/// </summary>
[DataField]
public SoundSpecifier SoundRemoveItem = new SoundPathSpecifier("/Audio/Nyanotrasen/Machines/deepfryer_basket_remove_item.ogg");
}
@@ -0,0 +1,5 @@
namespace Content.Server.Kitchen.Components
{
[RegisterComponent]
public sealed partial class ProfessionalChefComponent : Component {}
}
@@ -0,0 +1,186 @@
using System.Text;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Shared.Body.Components;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Buckle.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nyanotrasen.Kitchen.Components;
using Content.Shared.Paper;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem
{
private HashSet<ProtoId<FlavorPrototype>> _badFlavors = new();
private HashSet<ProtoId<FlavorPrototype>> _goodFlavors = new();
/// <summary>
/// Make an item look deep-fried.
/// </summary>
private void MakeCrispy(EntityUid item)
{
EnsureComp<AppearanceComponent>(item);
EnsureComp<DeepFriedComponent>(item);
_appearanceSystem.SetData(item, DeepFriedVisuals.Fried, true);
}
/// <summary>
/// Turn a dead mob into food.
/// </summary>
/// <remarks>
/// This is meant to be an irreversible process, similar to gibbing.
/// </remarks>
public bool TryMakeMobIntoFood(EntityUid mob, MobStateComponent mobStateComponent, bool force = false)
{
// Don't do anything to mobs until they're dead.
if (force || _mobStateSystem.IsDead(mob, mobStateComponent))
{
RemComp<ActiveNPCComponent>(mob);
RemComp<AtmosExposedComponent>(mob);
RemComp<BarotraumaComponent>(mob);
RemComp<BuckleComponent>(mob);
RemComp<GhostTakeoverAvailableComponent>(mob);
RemComp<PerishableComponent>(mob);
RemComp<RespiratorComponent>(mob);
RemComp<RottingComponent>(mob);
// Ensure it's Food here, so it passes the whitelist.
var mobFoodComponent = EnsureComp<EdibleComponent>(mob);
_solutionContainerSystem.EnsureSolution(mob, mobFoodComponent.Solution, out var alreadyHadFood);
if (!_solutionContainerSystem.TryGetSolution(mob, mobFoodComponent.Solution, out var mobFoodSolution))
return false;
// This line here is mainly for mice, because they have a food
// component that mirrors how much blood they have, which is
// used for the reagent grinder.
if (alreadyHadFood)
_solutionContainerSystem.RemoveAllSolution(mobFoodSolution.Value);
if (TryComp<BloodstreamComponent>(mob, out var bloodstreamComponent) && bloodstreamComponent.ChemicalSolution != null)
{
// Fry off any blood into protein.
var bloodSolution = bloodstreamComponent.BloodSolution;
var solPresent = bloodSolution!.Value.Comp.Solution.Volume;
_solutionContainerSystem.RemoveReagent(bloodSolution.Value, "Blood", FixedPoint2.MaxValue);
var bloodRemoved = solPresent - bloodSolution.Value.Comp.Solution.Volume;
var proteinQuantity = bloodRemoved * BloodToProteinRatio;
mobFoodSolution.Value.Comp.Solution.MaxVolume += proteinQuantity;
_solutionContainerSystem.TryAddReagent(mobFoodSolution.Value, "Protein", proteinQuantity);
// This is a heuristic. If you had blood, you might just taste meaty.
if (bloodRemoved > FixedPoint2.Zero)
EnsureComp<FlavorProfileComponent>(mob).Flavors.Add(MobFlavorMeat);
// Bring in whatever chemicals they had in them too.
mobFoodSolution.Value.Comp.Solution.MaxVolume +=
bloodstreamComponent.ChemicalSolution.Value.Comp.Solution.Volume;
_solutionContainerSystem.AddSolution(mobFoodSolution.Value,
bloodstreamComponent.ChemicalSolution.Value.Comp.Solution);
}
return true;
}
return false;
}
/// <summary>
/// Make an item actually edible.
/// </summary>
private void MakeEdible(EntityUid uid, DeepFryerComponent component, EntityUid item, FixedPoint2 solutionQuantity)
{
if (!TryComp<DeepFriedComponent>(item, out var deepFriedComponent))
{
_sawmill.Error($"{ToPrettyString(item)} is missing the DeepFriedComponent before being made Edible.");
return;
}
// Remove any components that wouldn't make sense anymore.
RemComp<ButcherableComponent>(item);
if (TryComp<PaperComponent>(item, out var paperComponent))
{
var stringBuilder = new StringBuilder();
for (var i = 0; i < paperComponent.Content.Length; ++i)
{
var uchar = paperComponent.Content.Substring(i, 1);
if (uchar == "\n" || _random.Prob(0.4f))
stringBuilder.Append(uchar);
else
stringBuilder.Append("x");
}
paperComponent.Content = stringBuilder.ToString();
}
var foodComponent = EnsureComp<EdibleComponent>(item);
var extraSolution = new Solution();
if (TryComp(item, out FlavorProfileComponent? flavorProfileComponent))
{
_goodFlavors.Clear();
_goodFlavors.IntersectWith(component.GoodFlavors);
_badFlavors.Clear();
_badFlavors.IntersectWith(component.BadFlavors);
deepFriedComponent.PriceCoefficient = Math.Max(0.01f,
1.0f
+ _goodFlavors.Count * component.GoodFlavorPriceBonus
- _badFlavors.Count * component.BadFlavorPriceMalus);
if (_goodFlavors.Count > 0)
{
foreach (var reagent in component.GoodReagents)
{
extraSolution.AddReagent(reagent.Reagent.ToString(), reagent.Quantity * _goodFlavors.Count);
// Mask the taste of "medicine."
flavorProfileComponent.IgnoreReagents.Add(reagent.Reagent.ToString());
}
}
if (_badFlavors.Count > 0)
{
foreach (var reagent in component.BadReagents)
{
extraSolution.AddReagent(reagent.Reagent.ToString(), reagent.Quantity * _badFlavors.Count);
}
}
}
else
{
flavorProfileComponent = EnsureComp<FlavorProfileComponent>(item);
// TODO: Default flavor?
}
// Make sure there's enough room for the fryer solution.
var foodSolution = _solutionContainerSystem.EnsureSolution(item, foodComponent.Solution);
if (!_solutionContainerSystem.TryGetSolution(item, foodSolution.Name, out var foodContainer))
return;
// The solution quantity is used to give the fried food an extra
// buffer too, to support injectables or condiments.
foodSolution.MaxVolume = 2 * solutionQuantity + foodSolution.Volume + extraSolution.Volume;
_solutionContainerSystem.AddSolution(foodContainer.Value,
component.Solution.SplitSolution(solutionQuantity));
_solutionContainerSystem.AddSolution(foodContainer.Value, extraSolution);
_solutionContainerSystem.UpdateChemicals(foodContainer.Value);
}
}
@@ -0,0 +1,91 @@
using Content.Server.Kitchen.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Content.Shared.Storage;
using Content.Shared.Tools.Components;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem
{
public bool CanInsertItem(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
// Keep this consistent with the checks in TryInsertItem.
return HasComp<ItemComponent>(item) &&
!HasComp<StorageComponent>(item) &&
component.Storage.ContainedEntities.Count < component.StorageMaxEntities;
}
private bool TryInsertItem(EntityUid uid, DeepFryerComponent component, EntityUid user, EntityUid item)
{
if (!HasComp<ItemComponent>(item))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-interact-using-not-item"),
uid,
user);
return false;
}
if (HasComp<StorageComponent>(item))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-storage-no-fit",
("item", item)),
uid,
user);
return false;
}
if (component.Storage.ContainedEntities.Count >= component.StorageMaxEntities)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-storage-full"),
uid,
user);
return false;
}
if (!_handsSystem.TryDropIntoContainer(user, item, component.Storage))
return false;
AfterInsert(uid, component, item);
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(user)} put {ToPrettyString(item)} inside {ToPrettyString(uid)}.");
return true;
}
private void OnInteractUsing(EntityUid uid, DeepFryerComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
// By default, allow entities with SolutionTransfer or Tool
// components to perform their usual actions. Inserting them (if
// the chef really wants to) will be supported through the UI.
if (HasComp<SolutionTransferComponent>(args.Used) ||
HasComp<ToolComponent>(args.Used))
return;
if (TryInsertItem(uid, component, args.User, args.Used))
args.Handled = true;
}
private void OnInsertItem(EntityUid uid, DeepFryerComponent component, DeepFryerInsertItemMessage args)
{
var user = args.Actor;
if (!TryComp<HandsComponent>(user, out var handsComponent) ||
_handsSystem.GetActiveItem((user, handsComponent)) is not { } item)
return;
TryInsertItem(uid, component, user, item);
}
}
@@ -0,0 +1,110 @@
using System.Linq;
using Content.Server.Kitchen.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Content.Shared.Popups;
using Robust.Shared.Player;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var component in EntityManager.EntityQuery<DeepFryerComponent>())
{
var uid = component.Owner;
if (_gameTimingSystem.CurTime < component.NextFryTime ||
!_powerReceiverSystem.IsPowered(uid))
{
continue;
}
UpdateNextFryTime(uid, component);
if (!_solutionContainerSystem.TryGetSolution(uid, component.Solution.Name, out var solution))
continue;
// Heat the vat solution and contained entities.
_solutionContainerSystem.SetTemperature(solution.Value, component.PoweredTemperature);
foreach (var item in component.Storage.ContainedEntities)
CookItem(uid, component, item);
// Do something bad if there's enough heat but not enough oil.
var oilVolume = GetOilVolume(uid, component);
if (oilVolume < component.SafeOilVolume)
{
foreach (var item in component.Storage.ContainedEntities.ToArray())
BurnItem(uid, component, item);
if (oilVolume > FixedPoint2.Zero)
{
component.Solution.RemoveAllSolution();
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-oil-volume-low",
("deepFryer", uid)),
uid,
PopupType.SmallCaution);
continue;
}
}
// We only alert the chef that there's a problem with oil purity
// if there's anything to cook beyond this point.
if (!component.Storage.ContainedEntities.Any())
{
continue;
}
if (GetOilPurity(uid, component) < component.FryingOilThreshold)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-oil-purity-low",
("deepFryer", uid)),
uid,
Filter.Pvs(uid, PvsWarningRange),
true);
continue;
}
foreach (var item in component.Storage.ContainedEntities.ToArray())
DeepFry(uid, component, item);
// After the round of frying, replace the spent oil with a
// waste product.
if (component.WasteToAdd > FixedPoint2.Zero)
{
foreach (var reagent in component.WasteReagents)
component.Solution.AddReagent(reagent.Reagent.ToString(), reagent.Quantity * component.WasteToAdd);
component.WasteToAdd = FixedPoint2.Zero;
_solutionContainerSystem.UpdateChemicals(solution.Value, true);
}
UpdateUserInterface(uid, component);
}
}
private void UpdateAmbientSound(EntityUid uid, DeepFryerComponent component)
{
_ambientSoundSystem.SetAmbience(uid, HasBubblingOil(uid, component));
}
private void UpdateNextFryTime(EntityUid uid, DeepFryerComponent component)
{
component.NextFryTime = _gameTimingSystem.CurTime + component.FryInterval;
}
}
@@ -0,0 +1,732 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Audio;
using Content.Server.Cargo.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Construction;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Nutrition;
using Content.Server.Nutrition.Components;
using Content.Server.Nyanotrasen.Kitchen.Components;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Storage.EntitySystems;
using Content.Server.Temperature.Systems;
using Content.Server.UserInterface;
using Content.Shared.Cargo;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Construction;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.EntityEffects;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nyanotrasen.Kitchen;
using Content.Shared.Nyanotrasen.Kitchen.Components;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Temperature.Components;
using Content.Shared.Throwing;
using Content.Shared.UserInterface;
using Content.Shared.Whitelist;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Nyanotrasen.Kitchen.EntitySystems;
public sealed partial class DeepFryerSystem : SharedDeepfryerSystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IGameTiming _gameTimingSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SolutionTransferSystem _solutionTransferSystem = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly TemperatureSystem _temperature = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly AmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private static readonly string CookingDamageType = "Heat";
private static readonly float CookingDamageAmount = 10.0f;
private static readonly float PvsWarningRange = 0.5f;
private static readonly float ThrowMissChance = 0.25f;
private static readonly int MaximumCrispiness = 2;
private static readonly float BloodToProteinRatio = 0.1f;
private static readonly string MobFlavorMeat = "meaty";
private static readonly AudioParams
AudioParamsInsertRemove = new(0.5f, 1f, 5f, 1.5f, 1f, false, 0f, 0.2f);
private ISawmill _sawmill = default!;
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("deepfryer");
SubscribeLocalEvent<DeepFryerComponent, ComponentInit>(OnInitDeepFryer);
SubscribeLocalEvent<DeepFryerComponent, PowerChangedEvent>(OnPowerChange);
SubscribeLocalEvent<DeepFryerComponent, MachineDeconstructedEvent>(OnDeconstruct);
SubscribeLocalEvent<DeepFryerComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<DeepFryerComponent, ThrowHitByEvent>(OnThrowHitBy);
SubscribeLocalEvent<DeepFryerComponent, SolutionChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<DeepFryerComponent, ContainerRelayMovementEntityEvent>(OnRelayMovement);
SubscribeLocalEvent<DeepFryerComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<DeepFryerComponent, BeforeActivatableUIOpenEvent>(OnBeforeActivatableUIOpen);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerRemoveItemMessage>(OnRemoveItem);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerInsertItemMessage>(OnInsertItem);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerScoopVatMessage>(OnScoopVat);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerClearSlagMessage>(OnClearSlagStart);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerRemoveAllItemsMessage>(OnRemoveAllItems);
SubscribeLocalEvent<DeepFryerComponent, ClearSlagDoAfterEvent>(OnClearSlag);
SubscribeLocalEvent<DeepFriedComponent, ComponentInit>(OnInitDeepFried);
SubscribeLocalEvent<DeepFriedComponent, ExaminedEvent>(OnExamineFried);
SubscribeLocalEvent<DeepFriedComponent, PriceCalculationEvent>(OnPriceCalculation);
SubscribeLocalEvent<DeepFriedComponent, FoodSlicedEvent>(OnSliceDeepFried);
}
private void UpdateUserInterface(EntityUid uid, DeepFryerComponent component)
{
var state = new DeepFryerBoundUserInterfaceState(
GetOilLevel(uid, component),
GetOilPurity(uid, component),
component.FryingOilThreshold,
EntityManager.GetNetEntityArray(component.Storage.ContainedEntities.ToArray()));
_uiSystem.SetUiState(uid, DeepFryerUiKey.Key, state);
}
/// <summary>
/// Does the deep fryer have hot oil?
/// </summary>
/// <remarks>
/// This is mainly for audio.
/// </remarks>
private bool HasBubblingOil(EntityUid uid, DeepFryerComponent component)
{
return _powerReceiverSystem.IsPowered(uid) && GetOilVolume(uid, component) > FixedPoint2.Zero;
}
/// <summary>
/// Returns how much total oil is in the vat.
/// </summary>
public FixedPoint2 GetOilVolume(EntityUid uid, DeepFryerComponent component)
{
var oilVolume = FixedPoint2.Zero;
foreach (var reagent in component.Solution)
{
if (component.FryingOils.Contains(reagent.Reagent.ToString()))
oilVolume += reagent.Quantity;
}
return oilVolume;
}
/// <summary>
/// Returns how much total waste is in the vat.
/// </summary>
public FixedPoint2 GetWasteVolume(EntityUid uid, DeepFryerComponent component)
{
var wasteVolume = FixedPoint2.Zero;
foreach (var reagent in component.WasteReagents)
{
wasteVolume += component.Solution.GetReagentQuantity(reagent.Reagent);
}
return wasteVolume;
}
/// <summary>
/// Returns a percentage of how much of the total solution is usable oil.
/// </summary>
public FixedPoint2 GetOilPurity(EntityUid uid, DeepFryerComponent component)
{
if (component.Solution.Volume == 0) return 0;
return GetOilVolume(uid, component) / component.Solution.Volume;
}
/// <summary>
/// Returns a percentage of how much of the total volume is usable oil.
/// </summary>
public FixedPoint2 GetOilLevel(EntityUid uid, DeepFryerComponent component)
{
return GetOilVolume(uid, component) / component.Solution.MaxVolume;
}
/// <summary>
/// This takes care of anything that would happen to an item with or
/// without enough oil.
/// </summary>
private void CookItem(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (TryComp<TemperatureComponent>(item, out var tempComp))
{
// Push the temperature towards what it should be but no higher.
var delta = (component.PoweredTemperature - tempComp.CurrentTemperature) * _temperature.GetHeatCapacity(item, tempComp);
if (delta > 0f)
_temperature.ChangeHeat(item, delta, false, tempComp);
}
if (TryComp<SolutionContainerManagerComponent>(item, out var solutions) && solutions.Solutions != null)
{
foreach (var (_, solution) in solutions.Solutions)
{
if(_solutionContainerSystem.TryGetSolution(item, solution.Name, out var solutionRef))
_solutionContainerSystem.SetTemperature(solutionRef!.Value, component.PoweredTemperature);
}
}
// Damage non-food items and mobs.
if ((!HasComp<EdibleComponent>(item) || HasComp<MobStateComponent>(item)) &&
TryComp<DamageableComponent>(item, out var damageableComponent))
{
var damage = new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>(CookingDamageType),
CookingDamageAmount);
var result = _damageableSystem.TryChangeDamage(item, damage, origin: uid);
if (result)
{
// TODO: Smoke, waste, sound, or some indication.
}
}
}
/// <summary>
/// Destroy a food item and replace it with a charred mess.
/// </summary>
private void BurnItem(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (HasComp<EdibleComponent>(item) &&
!HasComp<MobStateComponent>(item) &&
MetaData(item).EntityPrototype?.ID != component.CharredPrototype)
{
var charred = Spawn(component.CharredPrototype, Transform(uid).Coordinates);
_containerSystem.Insert(charred, component.Storage);
Del(item);
}
}
private void UpdateDeepFriedName(EntityUid uid, DeepFriedComponent component)
{
if (component.OriginalName == null)
return;
switch (component.Crispiness)
{
case 0:
// Already handled at OnInitDeepFried.
break;
case 1:
_metaDataSystem.SetEntityName(uid, Loc.GetString("deep-fried-crispy-item",
("entity", component.OriginalName)));
break;
default:
_metaDataSystem.SetEntityName(uid, Loc.GetString("deep-fried-burned-item",
("entity", component.OriginalName)));
break;
}
}
/// <summary>
/// Try to deep fry a single item, which can
/// - be cancelled by other systems, or
/// - fail due to the blacklist, or
/// - give it a crispy shader, and possibly also
/// - turn it into food.
/// </summary>
private void DeepFry(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (MetaData(item).EntityPrototype?.ID == component.CharredPrototype)
return;
// This item has already been deep-fried, and now it's progressing
// into another stage.
if (TryComp<DeepFriedComponent>(item, out var deepFriedComponent))
{
// TODO: Smoke, waste, sound, or some indication.
deepFriedComponent.Crispiness += 1;
if (deepFriedComponent.Crispiness > MaximumCrispiness)
{
BurnItem(uid, component, item);
return;
}
UpdateDeepFriedName(item, deepFriedComponent);
return;
}
// Allow entity systems to conditionally forbid an attempt at deep-frying.
var attemptEvent = new DeepFryAttemptEvent(uid);
RaiseLocalEvent(item, attemptEvent);
if (attemptEvent.Cancelled)
return;
// The attempt event is allowed to go first before the blacklist check,
// just in case the attempt is relevant to any system in the future.
//
// The blacklist overrides all.
if (component.Blacklist != null && _whitelistSystem.IsWhitelistPass(component.Blacklist, item))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-blacklist-item-failed",
("item", item), ("deepFryer", uid)),
uid,
Filter.Pvs(uid, PvsWarningRange),
true);
return;
}
var beingEvent = new BeingDeepFriedEvent(uid, item);
RaiseLocalEvent(item, beingEvent);
// It's important to check for the MobStateComponent so we know
// it's actually a mob, because functions like
// MobStateSystem.IsAlive will return false if the entity lacks the
// component.
if (TryComp<MobStateComponent>(item, out var mobStateComponent))
{
if (!TryMakeMobIntoFood(item, mobStateComponent))
return;
}
MakeCrispy(item);
var itemComponent = Comp<ItemComponent>(item);
// Determine how much solution to spend on this item.
var solutionQuantity = FixedPoint2.Min(
component.Solution.Volume,
itemComponent.Size.Id switch
{
"Tiny" => 1,
"Small" => 5,
"Medium" => 10,
"Large" => 15,
"Huge" => 30,
"Ginormous" => 50,
_ => 10
} * component.SolutionSizeCoefficient);
if (component.Whitelist != null && _whitelistSystem.IsWhitelistPass(component.Whitelist, item) ||
beingEvent.TurnIntoFood)
MakeEdible(uid, component, item, solutionQuantity);
else
component.Solution.RemoveSolution(solutionQuantity);
component.WasteToAdd += solutionQuantity;
}
private void OnInitDeepFryer(EntityUid uid, DeepFryerComponent component, ComponentInit args)
{
component.Storage =
_containerSystem.EnsureContainer<Container>(uid, component.StorageName, out var containerExisted);
if (!containerExisted)
_sawmill.Warning(
$"{ToPrettyString(uid)} did not have a {component.StorageName} container. It has been created.");
component.Solution =
_solutionContainerSystem.EnsureSolution(uid, component.SolutionName, out var solutionExisted);
if (!solutionExisted)
_sawmill.Warning(
$"{ToPrettyString(uid)} did not have a {component.SolutionName} solution container. It has been created.");
}
/// <summary>
/// Make sure the UI and interval tracker are updated anytime something
/// is inserted into one of the baskets.
/// </summary>
/// <remarks>
/// This is used instead of EntInsertedIntoContainerMessage so charred
/// items can be inserted into the deep fryer without triggering this
/// event.
/// </remarks>
private void AfterInsert(EntityUid uid, DeepFryerComponent component, EntityUid item)
{
if (HasBubblingOil(uid, component))
_audioSystem.PlayPvs(component.SoundInsertItem, uid, AudioParamsInsertRemove);
UpdateNextFryTime(uid, component);
UpdateUserInterface(uid, component);
}
private void OnPowerChange(EntityUid uid, DeepFryerComponent component, ref PowerChangedEvent args)
{
_appearanceSystem.SetData(uid, DeepFryerVisuals.Bubbling, args.Powered);
UpdateNextFryTime(uid, component);
UpdateAmbientSound(uid, component);
}
private void OnDeconstruct(EntityUid uid, DeepFryerComponent component, MachineDeconstructedEvent args)
{
// The EmptyOnMachineDeconstruct component handles the entity container for us.
_puddleSystem.TrySpillAt(uid, component.Solution, out var _);
}
private void OnDestruction(EntityUid uid, DeepFryerComponent component, DestructionEventArgs args)
{
_containerSystem.EmptyContainer(component.Storage, true);
}
/// <summary>
/// Allow thrown items to land in a basket.
/// </summary>
private void OnThrowHitBy(EntityUid uid, DeepFryerComponent component, ThrowHitByEvent args)
{
// Chefs never miss this. :)
var missChance = HasComp<ProfessionalChefComponent>(args.Component.Thrower) ? 0f : ThrowMissChance;
if (!CanInsertItem(uid, component, args.Thrown) ||
_random.Prob(missChance) ||
!_containerSystem.Insert(args.Thrown, component.Storage))
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-thrown-missed"),
uid);
if (args.Component.Thrower != null)
{
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Component.Thrower.Value)} threw {ToPrettyString(args.Thrown)} at {ToPrettyString(uid)}, and it missed.");
}
return;
}
if (GetOilVolume(uid, component) < component.SafeOilVolume)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-thrown-hit-oil-low"),
uid);
}
else
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-thrown-hit-oil"),
uid);
}
if (args.Component.Thrower != null)
{
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(args.Component.Thrower.Value)} threw {ToPrettyString(args.Thrown)} at {ToPrettyString(uid)}, and it landed inside.");
}
AfterInsert(uid, component, args.Thrown);
}
private void OnSolutionChange(EntityUid uid, DeepFryerComponent component, SolutionChangedEvent args)
{
UpdateUserInterface(uid, component);
UpdateAmbientSound(uid, component);
}
private void OnRelayMovement(EntityUid uid, DeepFryerComponent component,
ref ContainerRelayMovementEntityEvent args)
{
if (!_containerSystem.Remove(args.Entity, component.Storage, destination: Transform(uid).Coordinates))
return;
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-entity-escape",
("victim", Identity.Entity(args.Entity, EntityManager)),
("deepFryer", uid)),
uid,
PopupType.SmallCaution);
}
private void OnBeforeActivatableUIOpen(EntityUid uid, DeepFryerComponent component,
BeforeActivatableUIOpenEvent args)
{
UpdateUserInterface(uid, component);
}
private void OnRemoveItem(EntityUid uid, DeepFryerComponent component, DeepFryerRemoveItemMessage args)
{
var removedItem = EntityManager.GetEntity(args.Item);
if (removedItem.Valid)
{
//JJ Comment - This line should be unnecessary. Some issue is keeping the UI from updating when converting straight to a Burned Mess while the UI is still open. To replicate, put a Raw Meat in the fryer with no oil in it. Wait until it sputters with no effect. It should transform to Burned Mess, but doesn't.
if (!_containerSystem.Remove(removedItem, component.Storage))
return;
var user = args.Actor;
_handsSystem.TryPickupAnyHand(user, removedItem);
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(user)} took {ToPrettyString(args.Item)} out of {ToPrettyString(uid)}.");
_audioSystem.PlayPvs(component.SoundRemoveItem, uid, AudioParamsInsertRemove);
UpdateUserInterface(component.Owner, component);
}
}
/// <summary>
/// This is a helper function for ScoopVat and ClearSlag.
/// </summary>
private bool TryGetActiveHandSolutionContainer(
EntityUid fryer,
EntityUid user,
[NotNullWhen(true)] out EntityUid? heldItem,
[NotNullWhen(true)] out Entity<SolutionComponent>? solution,
out FixedPoint2 transferAmount)
{
heldItem = null;
solution = null;
transferAmount = FixedPoint2.Zero;
if (!TryComp<HandsComponent>(user, out var handsComponent))
return false;
heldItem = _handsSystem.GetActiveItem(user);
if (heldItem == null ||
!TryComp<SolutionTransferComponent>(heldItem, out var solutionTransferComponent) ||
!_solutionContainerSystem.TryGetRefillableSolution(heldItem.Value, out var solEnt, out var _) ||
!solutionTransferComponent.CanReceive)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-need-liquid-container-in-hand"),
fryer,
user);
return false;
}
solution = solEnt;
transferAmount = solutionTransferComponent.TransferAmount;
return true;
}
private void OnScoopVat(EntityUid uid, DeepFryerComponent component, DeepFryerScoopVatMessage args)
{
var user = args.Actor;
if (user == null ||
!TryGetActiveHandSolutionContainer(uid, user, out var heldItem, out var heldSolution,
out var transferAmount))
return;
if (!_solutionContainerSystem.TryGetSolution(component.Owner, component.Solution.Name, out var solution))
return;
_solutionTransferSystem.Transfer(new SolutionTransferData(user,
uid,
solution.Value,
heldItem.Value,
heldSolution.Value,
transferAmount));
// UI update is not necessary here, because the solution change event handles it.
}
private void OnClearSlagStart(EntityUid uid, DeepFryerComponent component, DeepFryerClearSlagMessage args)
{
var user = args.Actor;
if (!TryGetActiveHandSolutionContainer(uid, user, out var heldItem, out var heldSolution,
out var transferAmount))
return;
var wasteVolume = GetWasteVolume(uid, component);
if (wasteVolume == FixedPoint2.Zero)
{
_popupSystem.PopupEntity(
Loc.GetString("deep-fryer-oil-no-slag"),
uid,
user);
return;
}
var delay = TimeSpan.FromSeconds(Math.Clamp((float) wasteVolume * 0.1f, 1f, 5f));
var ev = new ClearSlagDoAfterEvent(heldSolution.Value.Comp.Solution, transferAmount);
//JJ Comment - not sure I have DoAfterArgs configured correctly.
var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, uid, heldItem)
{
BreakOnDamage = true,
BreakOnMove = true,
MovementThreshold = 0.25f,
NeedHand = true
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
}
[Obsolete("Obsolete")]
private void OnRemoveAllItems(EntityUid uid, DeepFryerComponent component, DeepFryerRemoveAllItemsMessage args)
{
if (component.Storage.ContainedEntities.Count == 0)
return;
_containerSystem.EmptyContainer(component.Storage);
var user = args.Actor;
_adminLogManager.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(user)} removed all items from {ToPrettyString(uid)}.");
_audioSystem.PlayPvs(component.SoundRemoveItem, uid, AudioParamsInsertRemove);
UpdateUserInterface(component.Owner, component);
}
private void OnClearSlag(EntityUid uid, DeepFryerComponent component, ClearSlagDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Used == null)
return;
FixedPoint2 reagentCount = component.WasteReagents.Count();
var removingSolution = new Solution();
foreach (var reagent in component.WasteReagents)
{
var removed = component.Solution.RemoveReagent(reagent.Reagent.ToString(), args.Amount / reagentCount);
removingSolution.AddReagent(reagent.Reagent.ToString(), removed);
}
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution))
return;
if (!_solutionContainerSystem.TryGetSolution(args.Used!.Value, args.Solution.Name, out var targetSolution))
return;
_solutionContainerSystem.UpdateChemicals(solution.Value);
_solutionContainerSystem.TryMixAndOverflow(targetSolution.Value, removingSolution,
args.Solution.MaxVolume, out var _);
}
private void OnInitDeepFried(EntityUid uid, DeepFriedComponent component, ComponentInit args)
{
var meta = MetaData(uid);
component.OriginalName = meta.EntityName;
_metaDataSystem.SetEntityName(uid, Loc.GetString("deep-fried-crispy-item", ("entity", meta.EntityName)));
}
private void OnExamineFried(EntityUid uid, DeepFriedComponent component, ExaminedEvent args)
{
switch (component.Crispiness)
{
case 0:
args.PushMarkup(Loc.GetString("deep-fried-crispy-item-examine"));
break;
case 1:
args.PushMarkup(Loc.GetString("deep-fried-fried-item-examine"));
break;
default:
args.PushMarkup(Loc.GetString("deep-fried-burned-item-examine"));
break;
}
}
private void OnPriceCalculation(EntityUid uid, DeepFriedComponent component, ref PriceCalculationEvent args)
{
args.Price *= component.PriceCoefficient;
}
private void OnSliceDeepFried(EntityUid uid, DeepFriedComponent component, FoodSlicedEvent args)
{
MakeCrispy(args.Slice);
// Copy relevant values to the slice.
var sourceDeepFriedComponent = Comp<DeepFriedComponent>(args.Food);
var sliceDeepFriedComponent = Comp<DeepFriedComponent>(args.Slice);
sliceDeepFriedComponent.Crispiness = sourceDeepFriedComponent.Crispiness;
sliceDeepFriedComponent.PriceCoefficient = sourceDeepFriedComponent.PriceCoefficient;
UpdateDeepFriedName(args.Slice, sliceDeepFriedComponent);
// TODO: Flavor profiles aren't copied to the slices. This should
// probably be handled on upstream, but for now let's assume the
// oil of the deep fryer is overpowering enough for this small
// hack. This is likely the only place where it would be useful.
if (TryComp<FlavorProfileComponent>(args.Food, out var sourceFlavorProfileComponent) &&
TryComp<FlavorProfileComponent>(args.Slice, out var sliceFlavorProfileComponent))
{
sliceFlavorProfileComponent.Flavors.UnionWith(sourceFlavorProfileComponent.Flavors);
sliceFlavorProfileComponent.IgnoreReagents.UnionWith(sourceFlavorProfileComponent.IgnoreReagents);
}
}
}
public sealed class DeepFryAttemptEvent : CancellableEntityEventArgs
{
public EntityUid DeepFryer { get; }
public DeepFryAttemptEvent(EntityUid deepFryer)
{
DeepFryer = deepFryer;
}
}
public sealed class BeingDeepFriedEvent : EntityEventArgs
{
public EntityUid DeepFryer { get; }
public EntityUid Item { get; }
public bool TurnIntoFood { get; set; }
public BeingDeepFriedEvent(EntityUid deepFryer, EntityUid item)
{
DeepFryer = deepFryer;
Item = item;
}
}
@@ -1,8 +1,9 @@
namespace Content.Server._DV.Objectives.Components;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Use this to mark a player as immune to any target objectives, useful for ghost roles or events.
/// </summary>
[RegisterComponent]
public sealed partial class TargetObjectiveImmuneComponent : Component;
public sealed partial class TargetObjectiveImmuneComponent : Component
{
}
@@ -1,7 +1,13 @@
using Content.Server._DV.Objectives.Components; // DeltaV
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles; // DeltaV
using Content.Shared.Roles.Jobs; // DeltaV
using Content.Server.GameTicking.Rules;
using Content.Server.Revolutionary.Components;
using Robust.Shared.Prototypes; // DeltaV
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
@@ -13,6 +19,10 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
{
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!; // DeltaV
[Dependency] private readonly IPrototypeManager _proto = default!; // DeltaV
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
@@ -48,14 +58,6 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
return;
}
// DeltaV - TargetObjectiveImmune
if (HasComp<TargetObjectiveImmuneComponent>(targetComp.Target))
{
args.Cancelled = true;
return;
}
// END DeltaV
_target.SetTarget(ent.Owner, targetComp.Target.Value);
}
@@ -79,17 +81,6 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
return;
}
// DeltaV - TargetObjectiveImmune
// Pretty much just a back-up check. Ideally, we should have filtered out all the minds
// with this comp with the mind filter TargetObjectiveMindFilter.
if (HasComp<TargetObjectiveImmuneComponent>(picked))
{
args.Cancelled = true;
return;
}
// END DeltaV
_target.SetTarget(ent, picked, target);
}
}
@@ -172,7 +172,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
_tracking.QueueRefreshTrackers(ev.PlayerSession);
// Send timers to client when they join lobby, so the UIs are up-to-date.
_tracking.QueueSendTimers(ev.PlayerSession);
_tracking.QueueSendWhitelist(ev.PlayerSession); // DeltaV - Send whitelist status
_tracking.QueueSendWhitelist(ev.PlayerSession); // Nyanotrasen - Send whitelist status
}
private void OnStationJobsGetCandidates(ref StationJobsGetCandidatesEvent ev)
@@ -258,7 +258,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
_prototypes,
(HumanoidCharacterProfile?)
_preferencesManager.GetPreferences(player.UserId).SelectedCharacter,
isWhitelisted: isWhitelisted); // DeltaV
isWhitelisted: isWhitelisted);
}
/// <summary>
@@ -278,7 +278,6 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
playTimes = new Dictionary<string, TimeSpan>();
}
var isWhitelisted = player.ContentData()?.Whitelisted ?? false; // DeltaV - Whitelist requirement
var requirements = _roles.GetRoleRequirements(antag);
return JobRequirements.TryRequirementsMet(
requirements,
@@ -287,8 +286,7 @@ public sealed class PlayTimeTrackingSystem : EntitySystem
EntityManager,
_prototypes,
(HumanoidCharacterProfile?)
_preferencesManager.GetPreferences(player.UserId).SelectedCharacter,
isWhitelisted: isWhitelisted); // DeltaV
_preferencesManager.GetPreferences(player.UserId).SelectedCharacter);
}
public HashSet<ProtoId<JobPrototype>> GetDisallowedJobs(ICommonSession player)
@@ -55,7 +55,6 @@ public sealed partial class RandomizedCandySystem : EntitySystem
_metaData.SetEntityName(uid, $"{candyFlavor.Name} {meta.EntityName}", meta);
_metaData.SetEntityDescription(uid, $"{meta.EntityDescription} {GetExamineFluff(candyFlavor.Flavors)}");
Dirty(uid, meta);
Dirty(uid, flavorProfile);
}
// this technically duplicates code from FlavorProfileSystem but what we would need to call
@@ -11,6 +11,7 @@ namespace Content.Server._DV.Abilities.Chitinid;
public sealed partial class ChitinidSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ItemCougherSystem _cougher = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
@@ -9,6 +9,7 @@ namespace Content.Server._DV.Abilities.Kitsune;
public sealed class KitsuneFoxSystem : EntitySystem
{
[Dependency] private readonly PolymorphSystem _polymorph = default!;
[Dependency] private readonly SharedStaminaSystem _stamina = default!;
public override void Initialize()
{
@@ -19,7 +20,7 @@ public sealed class KitsuneFoxSystem : EntitySystem
private void OnStunned(Entity<KitsuneFoxComponent> ent, ref StunnedEvent args)
{
if (!HasComp<PolymorphedEntityComponent>(ent))
if (!TryComp<PolymorphedEntityComponent>(ent, out var polymorph))
return;
_polymorph.Revert(ent.Owner);
@@ -27,6 +27,7 @@ public sealed class PrecognitionPowerSystem : EntitySystem
{
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -34,6 +35,7 @@ public sealed class PrecognitionPowerSystem : EntitySystem
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
@@ -25,7 +25,6 @@ public sealed class AddictionSystem : SharedAddictionSystem
private const int SuppressionDuration = 10;
private EntityQuery<AddictedComponent> _addicted;
private readonly ProtoId<LocalizedDatasetPrototype> _datasetId = "AddictionEffects";
public override void Initialize()
{
@@ -87,7 +86,7 @@ public sealed class AddictionSystem : SharedAddictionSystem
private string GetRandomPopup()
{
return Loc.GetString(_random.Pick(_prototypeManager.Index<LocalizedDatasetPrototype>(_datasetId).Values));
return Loc.GetString(_random.Pick(_prototypeManager.Index<LocalizedDatasetPrototype>("AddictionEffects").Values));
}
private void DoAddictionEffect(EntityUid uid)
@@ -24,6 +24,8 @@ public sealed class SpawnPlayer : LocalizedEntityCommands
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly TraitSystem _trait = default!;
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
public override string Command => "spawnplayer";
@@ -1,75 +0,0 @@
using Content.Server.Bible.Components;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Content.Server._DV.BibleUser;
/// <summary>
/// A system that tracks if the BibleUser has used a bible in the last X seconds (see <see cref="BibleUserComponent.Cooldown"/>).
/// This system is used to reset that cooldown.
/// <br />
/// <br />
/// See changes in Content.Server/Bible/BibleSystem.cs around the BibleUser component.
/// This system can easily be moved to shared once the upstream bible system is moved to shared.
/// </summary>
public sealed class BibleUserSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BibleUserComponent, ComponentInit>(OnComponentInit);
}
/// <summary>
/// Puts the usage of the Bible on cooldown.
/// </summary>
/// <param name="entity">The entity that may have the BibleUserComponent to put on cooldown.</param>
/// <param name="cooldown">If specified, the amount of time that the bible will be on cooldown.
/// If null, <see cref="BibleUserComponent.Cooldown"/> will be used.</param>
[PublicAPI]
public void StartBibleCooldown(Entity<BibleUserComponent?> entity, TimeSpan? cooldown = null)
{
if (!Resolve(entity, ref entity.Comp, false))
return;
// If they can't heal yet, don't reset the cooldown
if (!entity.Comp.CanHeal)
return;
if (!cooldown.HasValue)
cooldown = entity.Comp.Cooldown;
entity.Comp.CanHeal = false;
entity.Comp.NextUse = _timing.CurTime + cooldown.Value;
}
private void OnComponentInit(Entity<BibleUserComponent> entity, ref ComponentInit args)
{
entity.Comp.NextUse = _timing.CurTime;
}
/// <summary>
/// Updates if enough time has passed and the chaplain can use their bible again.
/// </summary>
/// <param name="frameTime"></param>
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<BibleUserComponent>();
while (query.MoveNext(out var uid, out var comp))
{
// If they can heal, we don't need to check if the cooldown time.
if (comp.CanHeal)
return;
if (_timing.CurTime > comp.NextUse)
{
comp.CanHeal = true;
}
}
}
}
@@ -89,7 +89,7 @@ public sealed partial class CargoSystem
{
Components =
[
Factory.GetComponentName<CargoShuttleComponent>()
Factory.GetComponentName(typeof(CargoShuttleComponent))
]
};
@@ -13,7 +13,7 @@ public sealed partial class PricingSystem
/// <returns>The modified price.</returns>
private double ApplyPrototypePriceModifier(EntityPrototype prototype, double basePrice)
{
if (prototype.Components.TryGetValue(Factory.GetComponentName<PriceModifierComponent>(),
if (prototype.Components.TryGetValue(Factory.GetComponentName(typeof(PriceModifierComponent)),
out var modProto))
{
var priceModifier = (PriceModifierComponent)modProto.Component;
@@ -109,7 +109,7 @@ public sealed class StockMarketSystem : EntitySystem
break;
default:
throw new InvalidOperationException($"Unknown UiAction type [{message.Action}]");
throw new ArgumentOutOfRangeException();
}
// Play confirmation sound if the transaction was successful
@@ -18,6 +18,7 @@ public sealed class CosmicConversionSystem : EntitySystem
[Dependency] private readonly CosmicCultRuleSystem _cultRule = default!;
[Dependency] private readonly CosmicGlyphSystem _cosmicGlyph = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedCosmicCultSystem _cosmicCult = default!;
@@ -1,5 +1,3 @@
using Content.Shared.Whitelist;
namespace Content.Server.Objectives.Components;
[RegisterComponent]
@@ -7,11 +5,4 @@ public sealed partial class CosmicEffigyConditionComponent : Component
{
[DataField]
public EntityUid? EffigyTarget;
/// <summary>
/// Tags that should be used to exclude Warp Points
/// from the list of valid effigy targets
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}
@@ -4,7 +4,6 @@ using Content.Shared.Ninja.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
using Content.Shared.Warps;
using Content.Shared.Whitelist;
using Robust.Shared.Random;
namespace Content.Server.Objectives.Systems;
@@ -15,7 +14,6 @@ public sealed class CosmicCultObjectiveSystem : EntitySystem
[Dependency] private readonly NumberObjectiveSystem _number = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
public override void Initialize()
{
@@ -37,13 +35,10 @@ public sealed class CosmicCultObjectiveSystem : EntitySystem
var warps = new List<EntityUid>();
var query = EntityQueryEnumerator<WarpPointComponent>();
var effigyBlacklist = comp.Blacklist;
// TODO: Add a blacklist comp to this like ninja now has from upstream #40726
while (query.MoveNext(out var warpUid, out var warp))
{
if (_whitelist.IsBlacklistFail(effigyBlacklist, warpUid)
&& !string.IsNullOrWhiteSpace(warp?.Location))
if (warp.Location != null)
{
warps.Add(warpUid);
}
@@ -74,6 +74,7 @@ public sealed class CosmicCultRuleSystem : GameRuleSystem<CosmicCultRuleComponen
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
[Dependency] private readonly EuiManager _euiMan = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
@@ -283,18 +284,9 @@ public sealed class CosmicCultRuleSystem : GameRuleSystem<CosmicCultRuleComponen
private void StewardVote()
{
// If there's already an entity with steward, don't hold a vote. This allows admins to add the Cosmic Cult rule a 2nd time
// in the case that there is only one cultist and they've been chosen as the steward already.
if (EntityQuery<CosmicCultLeadComponent>().Any())
{
_adminLogger.Add(LogType.Vote, LogImpact.Medium,
$"Cosmic cult steward already exists. Cancelling steward vote.");
return;
}
var cultists = new List<(string, EntityUid)>();
var cultQuery = EntityQueryEnumerator<CosmicCultComponent, MetaDataComponent>();
while (cultQuery.MoveNext(out var cult, out _, out var metadata))
{
var playerInfo = metadata.EntityName;
@@ -312,28 +304,11 @@ public sealed class CosmicCultRuleSystem : GameRuleSystem<CosmicCultRuleComponen
VoterEligibility = VoteManager.VoterEligibility.CosmicCult
};
// If there are no cultists, don't hold a vote, or the server will crash.
if (cultists.Count == 0)
{
Log.Warning($"There are no cosmic cultists present for the steward vote. Voting is cancelled to prevent the server crashing.");
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"There are no cosmic cultists for the steward vote. Steward vote is cancelled to prevent the server crashing.");
return;
}
foreach (var (name, ent) in cultists)
{
options.Options.Add((Loc.GetString(name), ent));
}
// If somehow there are cultists but no options, still don't hold a vote.
// Holding a vote with zero options crashes the server.
if (options.Options.Count == 0)
{
Log.Warning($"There are {cultists.Count} cosmic cultists but no options for the steward vote. Voting is cancelled to prevent the server crashing.");
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"There are {cultists.Count} cosmic cultists but no options for the steward vote. Steward vote is cancelled to prevent the server crashing.");
return;
}
var vote = _votes.CreateVote(options);
vote.OnFinished += (_, args) =>
@@ -27,12 +27,15 @@ namespace Content.Server._DV.CosmicCult;
public sealed class DeconversionSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedJitteringSystem _jittering = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedToolSystem _tools = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly IPlayerManager _playerMan = default!;
@@ -12,6 +12,7 @@ namespace Content.Server._DV.CosmicCult.EntitySystems;
public sealed partial class CosmicEntropyDegenSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
public override void Initialize()
@@ -54,7 +54,7 @@ public sealed partial class CwoinkSystem : SharedCwoinkSystem
private static partial Regex DiscordRegex();
private string _webhookUrl = string.Empty;
private WebhookData? _webhookData = null;
private WebhookData? _webhookData;
private readonly HttpClient _httpClient = new();
@@ -523,7 +523,8 @@ public sealed partial class CwoinkSystem : SharedCwoinkSystem
GameRunLevel.PreRoundLobby => "\n\n:arrow_forward: _**Pre-round lobby started**_\n",
GameRunLevel.InRound => "\n\n:arrow_forward: _**Round started**_\n",
GameRunLevel.PostRound => "\n\n:stop_button: _**Post-round started**_\n",
_ => throw new InvalidOperationException( $"{_gameTicker.RunLevel} was not matched."),
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel),
$"{_gameTicker.RunLevel} was not matched."),
};
existingEmbed.LastRunLevel = _gameTicker.RunLevel;
@@ -614,7 +615,8 @@ public sealed partial class CwoinkSystem : SharedCwoinkSystem
: $"pre-round lobby for round {_gameTicker.RoundId + 1}",
GameRunLevel.InRound => $"round {_gameTicker.RoundId}",
GameRunLevel.PostRound => $"post-round {_gameTicker.RoundId}",
_ => throw new InvalidOperationException( $"{_gameTicker.RunLevel} was not matched."),
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel),
$"{_gameTicker.RunLevel} was not matched."),
};
return new WebhookPayload
@@ -40,6 +40,7 @@ public sealed class ExecutionSystem : EntitySystem
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly InteractionSystem _interactionSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -261,7 +262,7 @@ public sealed class ExecutionSystem : EntitySystem
break;
default:
throw new InvalidOperationException($"Unknown shootable type [{ev.Ammo[0].Shootable}]");
throw new ArgumentOutOfRangeException();
}
// Clumsy people have a chance to shoot themselves (not in the head)
@@ -1,4 +0,0 @@
namespace Content.Server._DV.GameTicking.Rules.Components;
[RegisterComponent]
public sealed partial class NTAgentRuleComponent : Component;
@@ -44,6 +44,7 @@ public sealed partial class GrapplingSystem : SharedGrapplingSystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly StandingStateSystem _standingState = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedVirtualItemSystem _virtual = default!;
private ProtoId<TagPrototype> _grappleTargetId = "GrappleTarget";
@@ -1,677 +0,0 @@
using System.Linq;
using Content.Shared._DV.Kitchen;
using Content.Shared._DV.Kitchen.Components;
using Content.Shared._DV.Kitchen.Systems;
using Content.Shared.Audio;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Throwing;
using Content.Shared.Trigger.Systems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server._DV.Kitchen;
public sealed class DeepFryerSystem : SharedDeepFryerSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPowerReceiverSystem _power = default!;
[Dependency] private readonly TriggerSystem _trigger = default!;
/// <summary>
/// The trigger key used when non-frying oil reagents are added to the fryer
/// </summary>
public const string WrongReagentTriggerKey = "reaction";
private readonly List<EntityUid> _itemsToComplete = new();
private readonly List<EntityUid> _itemsToBurn = new();
private readonly HashSet<EntityUid> _processedItems = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeepFryerComponent, EntInsertedIntoContainerMessage>(OnItemInserted);
SubscribeLocalEvent<DeepFryerComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
SubscribeLocalEvent<DeepFryerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<DeepFryerComponent, SolutionTransferredEvent>(OnSolutionTransferred);
SubscribeLocalEvent<DeepFryerComponent, ThrowHitByEvent>(OnThrowHitBy);
}
private void OnSolutionTransferred(Entity<DeepFryerComponent> ent, ref SolutionTransferredEvent args)
{
// Only restore quality when oil is being added TO the fryer (not removed from it)
if (args.To != ent.Owner)
return;
// Get the fryer's solution to check what reagents are now in it
if (Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
{
// Check if any reagents in the solution are NOT valid frying oils
var hasInvalidReagent = false;
foreach (var reagent in solution.Contents)
{
if (!ent.Comp.FryingOils.Contains(reagent.Reagent.Prototype))
{
hasInvalidReagent = true;
break;
}
}
// If we found an invalid reagent, trigger the reaction
if (hasInvalidReagent)
{
_trigger.Trigger(ent, args.User, WrongReagentTriggerKey);
// Don't restore oil quality if we're triggering an explosion
return;
}
}
// Restore oil quality based on the amount transferred
var qualityRestored = (float)args.Amount * ent.Comp.OilQualityRestorationPerUnit;
ent.Comp.OilQuality = Math.Min(1.0f, ent.Comp.OilQuality + qualityRestored);
Dirty(ent);
}
private void OnPowerChanged(Entity<DeepFryerComponent> ent, ref PowerChangedEvent args)
{
UpdateAppearance(ent);
}
private void OnThrowHitBy(Entity<DeepFryerComponent> ent, ref ThrowHitByEvent args)
{
if (args.Component.Thrower is not { } thrower || !CanInsertItem(ent, args.Thrown, out _))
return;
if (!HasComp<ProfessionalChefComponent>(thrower) && _random.Prob(ent.Comp.MissChance))
{
// Item missed! Let it continue with normal throw physics
Popup.PopupEntity(Loc.GetString("deep-fryer-throw-miss", ("item", args.Thrown)), ent, thrower);
return;
}
// Success! Insert the item
if (TryInsertItem(ent, args.Thrown, thrower))
Popup.PopupEntity(Loc.GetString("deep-fryer-throw-success", ("item", args.Thrown)), ent, thrower);
}
private void OnItemInserted(Entity<DeepFryerComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != ent.Comp.ContainerName)
return;
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return;
// First, check if this new item completes any multi-ingredient recipes with items already in the fryer
var completedMultiRecipe = TryFindAndUpgradeToMultiRecipe(ent, container);
if (completedMultiRecipe == null)
{
// No multi-recipe was completed, so assign this item its best single-ingredient recipe
var singleRecipe = FindBestRecipeForItem(args.Entity);
ent.Comp.CookingItems[args.Entity] = new CookingItem(singleRecipe, _timing.CurTime);
}
UpdateAppearance(ent);
}
private void OnItemRemoved(Entity<DeepFryerComponent> ent, ref EntRemovedFromContainerMessage args)
{
// Only process items in the fryer basket
if (args.Container.ID != ent.Comp.ContainerName)
return;
// Remove from cooking tracking
ent.Comp.CookingItems.Remove(args.Entity);
UpdateAppearance(ent);
}
/// <summary>
/// Updates the visual appearance of the deep fryer based on power and cooking state
/// </summary>
private void UpdateAppearance(Entity<DeepFryerComponent> ent)
{
var isBubbling = false;
// Check if the fryer is powered and has items
if (_power.IsPowered(ent.Owner)
&& ent.Comp.CookingItems.Count > 0
&& HasEnoughOil(ent))
{
isBubbling = true;
}
UpdateAmbience(ent, isBubbling);
_appearance.SetData(ent, DeepFryerVisuals.Bubbling, isBubbling);
}
private void UpdateAmbience(Entity<DeepFryerComponent> ent, bool value)
{
_ambientSound.SetAmbience(ent, value);
}
/// <summary>
/// Finds the best recipe for a single item.
/// Prioritizes multi-ingredient recipes (returns null so item waits), then single-ingredient recipes.
/// Returns null if item is only in multi-ingredient recipes or has no recipe at all.
/// </summary>
private ProtoId<DeepFryerRecipePrototype>? FindBestRecipeForItem(EntityUid item)
{
var itemProto = MetaData(item).EntityPrototype?.ID;
if (itemProto == null)
return null;
ProtoId<DeepFryerRecipePrototype>? singleIngredientRecipe = null;
// Look through all deep fryer recipes
foreach (var deepFryerRecipe in _prototype.EnumeratePrototypes<DeepFryerRecipePrototype>())
{
// Get the base microwave recipe
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
continue;
// Count total solid ingredients
FixedPoint2 totalIngredients = 0;
var hasThisItem = false;
foreach (var (ingredientId, count) in microwaveRecipe.IngredientsSolids)
{
totalIngredients += count;
if (ingredientId == itemProto)
hasThisItem = true;
}
if (!hasThisItem)
continue;
if (totalIngredients == 1)
{
// This is a single-ingredient recipe
singleIngredientRecipe = deepFryerRecipe.ID;
}
}
// Return the single-ingredient recipe (may be null)
return singleIngredientRecipe;
}
/// <summary>
/// Checks if the newly inserted item completes any multi-ingredient recipe with existing items.
/// If so, upgrades all involved items to use that recipe.
/// Returns the recipe if one was found and upgraded, null otherwise.
/// </summary>
private ProtoId<DeepFryerRecipePrototype>? TryFindAndUpgradeToMultiRecipe(
Entity<DeepFryerComponent> ent,
BaseContainer container)
{
// Look through all multi-ingredient recipes to see if any are now complete
foreach (var deepFryerRecipe in _prototype.EnumeratePrototypes<DeepFryerRecipePrototype>())
{
// Get the base microwave recipe
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
continue;
// Count total solid ingredients
FixedPoint2 totalIngredients = 0;
foreach (var (_, count) in microwaveRecipe.IngredientsSolids)
{
totalIngredients += count;
}
// Skip single-ingredient recipes
if (totalIngredients <= 1)
continue;
// Check if all ingredients for this multi-ingredient recipe are present
var ingredients = GetIngredientsForRecipe(deepFryerRecipe.ID, container);
if (ingredients == null)
continue;
// Check if ingredients are within tolerance
if (!AreIngredientsWithinTolerance(ent, ingredients))
continue;
// We found a complete multi-ingredient recipe within tolerance!
// Upgrade all ingredients to use this recipe
// Find the earliest start time among all ingredients
var earliestTime = _timing.CurTime;
foreach (var (ingredientUid, _) in ingredients)
{
if (ent.Comp.CookingItems.TryGetValue(ingredientUid, out var existingItem))
{
if (existingItem.TimeStarted < earliestTime)
earliestTime = existingItem.TimeStarted;
}
}
// Assign the multi-ingredient recipe to all ingredients with synchronized start time
foreach (var (ingredientUid, _) in ingredients)
{
ent.Comp.CookingItems[ingredientUid] = new CookingItem(deepFryerRecipe.ID, earliestTime);
}
return deepFryerRecipe.ID;
}
return null;
}
/// <summary>
/// Tries to get all ingredients needed for a specific recipe from the fryer.
/// Returns null if not all ingredients are present.
/// </summary>
private Dictionary<EntityUid, string>? GetIngredientsForRecipe(
ProtoId<DeepFryerRecipePrototype> recipeId,
BaseContainer container)
{
if (!_prototype.TryIndex(recipeId, out var deepFryerRecipe))
return null;
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
return null;
var neededIngredients = new Dictionary<string, FixedPoint2>();
foreach (var (ingredient, count) in microwaveRecipe.IngredientsSolids)
{
neededIngredients[ingredient] = count;
}
var foundIngredients = new Dictionary<EntityUid, string>();
// Check each item in the fryer
foreach (var itemUid in container.ContainedEntities)
{
var itemProto = MetaData(itemUid).EntityPrototype?.ID;
if (itemProto == null)
continue;
// If this item is one of the needed ingredients
if (neededIngredients.TryGetValue(itemProto, out var needed) && needed > 0)
{
foundIngredients[itemUid] = itemProto;
neededIngredients[itemProto] -= 1;
}
}
// Check if we found all required ingredients
foreach (var (_, count) in neededIngredients)
{
if (count > 0)
return null; // Missing some ingredients
}
return foundIngredients;
}
/// <summary>
/// Checks if all ingredients for a multi-ingredient recipe are within the cooking time tolerance
/// </summary>
private bool AreIngredientsWithinTolerance(
Entity<DeepFryerComponent> ent,
Dictionary<EntityUid, string> ingredients)
{
if (ingredients.Count <= 1)
return true;
TimeSpan? earliest = null;
TimeSpan? latest = null;
foreach (var (ingredientUid, _) in ingredients)
{
if (!ent.Comp.CookingItems.TryGetValue(ingredientUid, out var cookingItem))
continue;
if (earliest == null || cookingItem.TimeStarted < earliest)
earliest = cookingItem.TimeStarted;
if (latest == null || cookingItem.TimeStarted > latest)
latest = cookingItem.TimeStarted;
}
if (earliest == null || latest == null)
return false;
var timeDifference = latest.Value - earliest.Value;
return timeDifference <= ent.Comp.CookingTolerance;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_itemsToComplete.Clear();
_itemsToBurn.Clear();
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<DeepFryerComponent>();
while (query.MoveNext(out var uid, out var fryer))
{
// Skip if not powered
if (!_power.IsPowered(uid))
continue;
// Skip if no oil
if (!HasEnoughOil((uid, fryer)))
continue;
// Get the container
if (!_container.TryGetContainer(uid, fryer.ContainerName, out var container))
continue;
_processedItems.Clear();
// Process each cooking item
foreach (var (itemUid, cookingItem) in fryer.CookingItems.ToList())
{
// Skip if already processed as part of a multi-ingredient recipe
if (_processedItems.Contains(itemUid))
continue;
var elapsedTime = curTime - cookingItem.TimeStarted;
// If the item is already marked as burning
if (cookingItem.IsBurning)
{
if (cookingItem.Recipe is { } burningRecipe)
{
if (!_prototype.TryIndex(burningRecipe, out var deepFryerRecipe))
continue;
var burnTime = deepFryerRecipe.BurnTime;
if (elapsedTime >= burnTime)
{
_itemsToBurn.Add(itemUid);
}
}
continue;
}
// Item is cooking (not burning yet)
if (cookingItem.Recipe is { } recipe)
{
if (!_prototype.TryIndex(recipe, out var deepFryerRecipe))
continue;
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
continue;
var cookTime = TimeSpan.FromSeconds(microwaveRecipe.CookTime);
// Check if this is part of a multi-ingredient recipe
var multiIngredients = GetIngredientsForRecipe(recipe, container);
if (multiIngredients is { Count: > 1 })
{
// This is a multi-ingredient recipe
// Check if all ingredients are still within tolerance
if (!AreIngredientsWithinTolerance((uid, fryer), multiIngredients))
continue;
// Find the earliest start time
var earliestStart = TimeSpan.MaxValue;
foreach (var (ingredientUid, _) in multiIngredients)
{
// TryGetValue in Update my beloved
// Only like one deep fryer per map so it's gonna be fine probably
if (!fryer.CookingItems.TryGetValue(ingredientUid, out var ingredientCookingItem))
continue;
if (ingredientCookingItem.TimeStarted < earliestStart)
earliestStart = ingredientCookingItem.TimeStarted;
}
// Check if enough time has passed since the earliest ingredient
var earliestElapsed = curTime - earliestStart;
if (earliestElapsed < cookTime)
continue;
{
// Mark the first item for completion (it will handle all ingredients)
_itemsToComplete.Add(itemUid);
// Mark all ingredients as processed
foreach (var (ingredientUid, _) in multiIngredients)
{
_processedItems.Add(ingredientUid);
}
}
// Note: If ingredients are outside tolerance, they keep their current recipes
// and will be handled individually (single-ingredient recipes will complete, items without recipes will burn)
}
else
{
// Single-ingredient recipe, proceed normally
if (elapsedTime >= cookTime)
{
_itemsToComplete.Add(itemUid);
}
}
}
else
{
// Item has no recipe assigned - it should burn after BaseBurnTime
if (elapsedTime >= fryer.BaseBurnTime)
{
_itemsToBurn.Add(itemUid);
}
}
}
// Complete cooking for finished items
foreach (var itemUid in _itemsToComplete)
{
CompleteCooking((uid, fryer), itemUid, container);
}
// Burn items that have been cooking too long or have no recipe
foreach (var itemUid in _itemsToBurn)
{
BurnItem((uid, fryer), itemUid, container);
}
}
}
/// <summary>
/// Completes cooking for an item (or multi-ingredient recipe), transforming it into the result
/// </summary>
private void CompleteCooking(Entity<DeepFryerComponent> ent, EntityUid item, BaseContainer container)
{
if (!ent.Comp.CookingItems.TryGetValue(item, out var cookingItem))
return;
if (cookingItem.Recipe is not { } recipe)
return;
if (!_prototype.TryIndex(recipe, out var deepFryerRecipe))
return;
// Get the base microwave recipe for result
if (!_prototype.Resolve(deepFryerRecipe.BaseRecipe, out var microwaveRecipe))
return;
// Get all ingredients for this recipe
var recipeIngredients = GetIngredientsForRecipe(recipe, container);
var isMultiIngredient = recipeIngredients is { Count: > 1 };
// Check if we should burn the item due to foul oil
var qualityLevel = GetOilQualityLevel(ent.Comp.OilQuality);
if (qualityLevel == OilQuality.Foul && _random.Prob(ent.Comp.FoulOilBurnChance))
{
// For multi-ingredient recipes, burn all ingredients
if (isMultiIngredient)
{
foreach (var (ingredientUid, _) in recipeIngredients!)
{
BurnItem(ent, ingredientUid, container, recipe: recipe);
}
return;
}
// Force burn the single item
BurnItem(ent, item, container, recipe: recipe);
return;
}
var xform = Transform(ent);
var coords = Xform.GetMapCoordinates((ent, xform));
// For multi-ingredient recipes, remove ALL ingredients
if (isMultiIngredient)
{
// Delete all ingredients
foreach (var (ingredientUid, _) in recipeIngredients!)
{
ent.Comp.CookingItems.Remove(ingredientUid);
_container.Remove(ingredientUid, container);
QueueDel(ingredientUid);
}
}
else
{
// Single ingredient recipe
ent.Comp.CookingItems.Remove(item);
_container.Remove(item, container);
QueueDel(item);
}
// Spawn the result (from the microwave recipe)
var result = Spawn(microwaveRecipe.Result, coords);
// Transfer solution from fryer to food (includes oil AND any contaminants!)
TransferOilToFood(ent, result, deepFryerRecipe.OilConsumption);
// Add flavors based on oil quality
AddOilQualityFlavors(result, ent.Comp, qualityLevel);
// Degrade oil quality
DegradeOilQuality(ent);
// Try to put it back in the fryer
if (!_container.Insert(result, container))
{
// If we can't insert it (container full?), just leave it at the fryer's location
Xform.SetCoordinates(result, xform, Xform.GetMoverCoordinates(ent, xform));
}
else
{
// Track the result and start burning timer
ent.Comp.CookingItems[result] = new CookingItem(cookingItem.Recipe, _timing.CurTime, isBurning: true);
}
// Show a popup
Popup.PopupEntity(Loc.GetString("deep-fryer-item-finished", ("item", result)), ent, PopupType.Medium);
_audio.PlayPvs(ent.Comp.FinishedCookingSound, ent);
}
/// <summary>
/// Burns an item - uses recipe's BurnedResult if available, otherwise uses BaseBurnedResult
/// </summary>
private void BurnItem(Entity<DeepFryerComponent> ent, EntityUid item, BaseContainer container, ProtoId<DeepFryerRecipePrototype>? recipe = null)
{
EntProtoId? burnedEntity = null;
// If we were never explicitly given a recipe, then see if there's one
if (!recipe.HasValue && ent.Comp.CookingItems.TryGetValue(item, out var cookingItem) && cookingItem.Recipe is { } foundRecipe)
recipe = foundRecipe;
// Try to get the recipe, if we were given one or if we found one
if (_prototype.TryIndex(recipe, out var deepFryerRecipe))
burnedEntity = deepFryerRecipe.BurnedResult;
// finally, if we don't have a BurnedResult from a recipe, just default to BaseBurnedResult
if (!burnedEntity.HasValue)
burnedEntity = ent.Comp.BaseBurnedResult;
// Remove the item from tracking and container
ent.Comp.CookingItems.Remove(item);
_container.Remove(item, container);
// Delete the original item
QueueDel(item);
// Spawn the burned result on top of the fryer
Spawn(burnedEntity, Xform.GetMoverCoordinates(ent));
// Degrade oil quality even when burning
DegradeOilQuality(ent);
// Show a danger popup
Popup.PopupEntity(Loc.GetString("deep-fryer-item-burned", ("item", item)), ent, PopupType.MediumCaution);
_audio.PlayPvs(ent.Comp.FinishedBurningSound, ent);
}
/// <summary>
/// Adds flavors to the cooked item based on the current oil quality
/// </summary>
private void AddOilQualityFlavors(EntityUid result, DeepFryerComponent fryer, OilQuality qualityLevel)
{
// Get or create the FlavorProfile component
var flavorProfile = EnsureComp<FlavorProfileComponent>(result);
// Get the flavors for this quality level
if (!fryer.OilQualityFlavors.TryGetValue(qualityLevel, out var flavors))
return;
// Add each flavor to the profile
foreach (var flavor in flavors)
{
flavorProfile.Flavors.Add(flavor);
}
Dirty(result, flavorProfile);
}
/// <summary>
/// Degrades the oil quality after cooking an item
/// </summary>
private void DegradeOilQuality(Entity<DeepFryerComponent> ent)
{
// Calculate degradation multiplier based on oil volume
var degradationMultiplier = CalculateOilDegradationMultiplier(ent);
// Reduce oil quality with the multiplier applied
var degradationAmount = ent.Comp.OilDegradationPerRecipe * degradationMultiplier;
ent.Comp.OilQuality = Math.Max(0f, ent.Comp.OilQuality - degradationAmount);
// Mark as dirty to sync to clients
Dirty(ent);
}
/// <summary>
/// Transfers solution from the fryer into the food solution.
/// Transfers ALL reagents proportionally - if someone added bleach to the fryer, enjoy your bleach-fried food!
/// </summary>
private void TransferOilToFood(Entity<DeepFryerComponent> ent, EntityUid food, FixedPoint2 amount)
{
// Get the fryer's solution
if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var fryerSolution))
return;
// Get the food solution
if (!Solution.TryGetSolution(food, "food", out var foodSolutionEnt, out _))
return;
// Split the desired amount from the fryer - this takes ALL reagents proportionally!
// If the fryer has 80% oil and 20% bleach, the food gets 80% oil and 20% bleach too!
var transferredSolution = fryerSolution.SplitSolution(amount);
// Add the split solution to the food
Solution.AddSolution(foodSolutionEnt.Value, transferredSolution);
}
}
@@ -6,11 +6,13 @@ using Content.Server._DV.Mail.Components;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Destructible.Thresholds.Triggers;
using Content.Server.Destructible;
using Content.Server.Mind;
using Content.Server.Power.Components;
using Content.Server.Radio.EntitySystems; // ImpStation - for radio notifications of new mail
using Content.Server.Spawners.EntitySystems;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access;
using Content.Shared.Cargo.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Components;
@@ -32,7 +34,6 @@ using Content.Shared.Cargo.Prototypes;
using Content.Shared.Power.EntitySystems;
using Timer = Robust.Shared.Timing.Timer;
using Content.Shared.Destructible;
using JetBrains.Annotations;
namespace Content.Server._DV.Mail.EntitySystems;
@@ -44,6 +45,7 @@ public sealed class MailSystem : SharedMailSystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _meta = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
@@ -236,8 +238,6 @@ public sealed class MailSystem : SharedMailSystem
/// <remarks>
/// This is separate mostly so the unit tests can get to it.
/// </remarks>
/// TODO: Move to shared when IsEntityFragile can be made network-safe
[PublicAPI]
public void SetupMail(EntityUid uid, MailTeleporterComponent component, MailRecipient recipient)
{
var mailComp = EnsureComp<MailComponent>(uid);
@@ -267,12 +267,14 @@ public sealed class MailSystem : SharedMailSystem
mailComp.RecipientJob = recipient.Job;
mailComp.Recipient = recipient.Name;
// Frontier: Large mail bonus
var mailEntityStrings = mailComp.IsLarge ? MailConstants.MailLarge : MailConstants.Mail;
if (mailComp.IsLarge)
{
mailComp.Bounty += component.LargeBonus;
mailComp.Penalty += component.LargeMalus;
}
// End Frontier
if (mailComp.IsFragile)
{
@@ -294,6 +296,7 @@ public sealed class MailSystem : SharedMailSystem
Timer.Spawn((int)component.PriorityDuration.TotalMilliseconds,
() =>
{
// DeltaV - Expired mail recorded to logistic stats
ExecuteForEachLogisticsStats(uid,
(station, logisticStats) =>
{
@@ -310,7 +313,7 @@ public sealed class MailSystem : SharedMailSystem
Appearance.SetData(uid, MailVisuals.JobIcon, recipient.JobIcon);
_meta.SetEntityName(uid,
Loc.GetString(mailEntityStrings.NameAddressed,
Loc.GetString(mailEntityStrings.NameAddressed, // Frontier: move constant to MailEntityString
("recipient", recipient.Name)));
var accessReader = EnsureComp<AccessReaderComponent>(uid);
@@ -348,6 +351,55 @@ public sealed class MailSystem : SharedMailSystem
return (uint)GetUndeliveredParcels(uid).Count;
}
/// <summary>
/// Try to match a mail receiver to a mail teleporter.
/// </summary>
public bool TryGetMailTeleporterForReceiver(EntityUid receiverUid, [NotNullWhen(true)] out MailTeleporterComponent? teleporterComponent, [NotNullWhen(true)] out EntityUid? teleporterUid)
{
var query = EntityQueryEnumerator<MailTeleporterComponent>();
var receiverStation = Station.GetOwningStation(receiverUid);
while (query.MoveNext(out var uid, out var mailTeleporter))
{
var teleporterStation = Station.GetOwningStation(uid);
if (receiverStation != teleporterStation)
continue;
teleporterComponent = mailTeleporter;
teleporterUid = uid;
return true;
}
teleporterComponent = null;
teleporterUid = null;
return false;
}
/// <summary>
/// Try to construct a recipient struct for a mail parcel based on a receiver.
/// </summary>
public bool TryGetMailRecipientForReceiver(EntityUid receiverUid, [NotNullWhen(true)] out MailRecipient? recipient)
{
if (IdCard.TryFindIdCard(receiverUid, out var idCard)
&& TryComp<AccessComponent>(idCard.Owner, out var access)
&& idCard.Comp.FullName != null)
{
var accessTags = access.Tags;
var mayReceivePriorityMail = !(_mind.GetMind(receiverUid) == null);
recipient = new MailRecipient(
idCard.Comp.FullName,
idCard.Comp.LocalizedJobTitle ?? idCard.Comp.JobTitle ?? "Unknown",
idCard.Comp.JobIcon,
accessTags,
mayReceivePriorityMail);
return true;
}
recipient = null;
return false;
}
/// <summary>
/// Get the list of valid mail recipients for a mail teleporter.
/// </summary>
@@ -392,7 +444,7 @@ public sealed class MailSystem : SharedMailSystem
return;
}
if (!_prototype.TryIndex(ent.Comp.MailPool, out var pool))
if (!_prototype.TryIndex<MailDeliveryPoolPrototype>(ent.Comp.MailPool, out var pool))
{
Log.Error($"Can't index {ToPrettyString(ent)}'s MailPool {ent.Comp.MailPool}!");
return;
@@ -446,7 +498,7 @@ public sealed class MailSystem : SharedMailSystem
var mail = EntityManager.SpawnEntity(chosenParcel, coordinates);
SetupMail(mail, ent.Comp, candidate);
Tag.AddTag(mail, MailTag);
Tag.AddTag(mail, MailTag); // Frontier
}
if (_container.TryGetContainer(ent, "queued", out var queued))
@@ -466,29 +518,18 @@ public sealed class MailSystem : SharedMailSystem
var message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args);
_radio.SendRadioMessage(source, message, channel, source);
}
/// <summary>
/// Sets the next delivery time for the mail teleporter.
/// </summary>
[PublicAPI]
public void SetNextDeliveryTime(Entity<MailTeleporterComponent?> ent, TimeSpan nextDeliveryTime)
{
if (!Resolve(ent, ref ent.Comp) || ent.Comp.NextDelivery == nextDeliveryTime)
return;
ent.Comp.NextDelivery = nextDeliveryTime;
}
/// <summary>
/// Triggers an immediate mail delivery for the teleporter, bypassing the normal timer.
/// </summary>
[PublicAPI]
public void DeliverNow(Entity<MailTeleporterComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
SpawnMail(ent);
ent.Comp.NextDelivery = Timing.CurTime + ent.Comp.TeleportInterval;
}
}
public struct MailRecipient(
string name,
string job,
string jobIcon,
HashSet<ProtoId<AccessLevelPrototype>> accessTags,
bool mayReceivePriorityMail)
{
public readonly string Name = name;
public readonly string Job = job;
public readonly string JobIcon = jobIcon;
public readonly HashSet<ProtoId<AccessLevelPrototype>> AccessTags = accessTags;
public readonly bool MayReceivePriorityMail = mayReceivePriorityMail;
}
+62 -67
View File
@@ -11,47 +11,24 @@ using Robust.Shared.Timing;
namespace Content.Server._DV.Mail;
[AdminCommand(AdminFlags.Fun)]
public sealed class MailToCommand : LocalizedEntityCommands
public sealed class MailToCommand : IConsoleCommand
{
public override string Command => "mailto";
public override string Description => Loc.GetString("cmd-mailto-description", ("requiredComponent", nameof(MailReceiverComponent)));
public override string Help => Loc.GetString("cmd-mailto-help", ("command", Command));
public string Command => "mailto";
public string Description => Loc.GetString("command-mailto-description", ("requiredComponent", nameof(MailReceiverComponent)));
public string Help => Loc.GetString("command-mailto-help", ("command", Command));
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly MailSystem _mail = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedMailSystem _sharedMail = default!;
private static readonly EntProtoId BlankMailPrototype = "MailAdminFun";
private static readonly EntProtoId BlankLargeMailPrototype = "MailLargeAdminFun";
private const string BlankMailPrototype = "MailAdminFun";
private const string BlankLargeMailPrototype = "MailLargeAdminFun"; // Frontier: large mail
private const string Container = "storagebase";
private const string MailContainer = "contents";
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 =>
CompletionResult.FromHintOptions(
CompletionHelper.Components<MailReceiverComponent>(args[0], EntityManager),
Loc.GetString("cmd-mailto-hint-recipient")),
2 =>
CompletionResult.FromHintOptions(CompletionHelper.NetEntities(args[1], EntityManager),
Loc.GetString("cmd-mailto-hint-container")),
3 =>
CompletionResult.FromHintOptions(CompletionHelper.Booleans,
Loc.GetString("cmd-mailto-hint-fragile")),
4 =>
CompletionResult.FromHintOptions(CompletionHelper.Booleans,
Loc.GetString("cmd-mailto-hint-priority")),
5 =>
CompletionResult.FromHintOptions(CompletionHelper.Booleans, Loc.GetString("cmd-mailto-hint-large")),
_ => CompletionResult.Empty
};
}
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 4)
{
@@ -59,18 +36,31 @@ public sealed class MailToCommand : LocalizedEntityCommands
return;
}
if (!EntityUid.TryParse(args[0], out var recipientUid) || !EntityUid.TryParse(args[1], out var containerUid))
if (!EntityUid.TryParse(args[0], out var recipientUid))
{
shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
return;
}
if (!bool.TryParse(args[2], out var isFragile) || !bool.TryParse(args[3], out var isPriority))
if (!EntityUid.TryParse(args[1], out var containerUid))
{
shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
return;
}
if (!bool.TryParse(args[2], out var isFragile))
{
shell.WriteError(Loc.GetString("shell-invalid-bool"));
return;
}
if (!bool.TryParse(args[3], out var isPriority))
{
shell.WriteError(Loc.GetString("shell-invalid-bool"));
return;
}
// Frontier: Large Mail
var isLarge = false;
if (args.Length > 4 && !bool.TryParse(args[4], out isLarge))
{
@@ -78,80 +68,85 @@ public sealed class MailToCommand : LocalizedEntityCommands
return;
}
var mailPrototype = isLarge ? BlankLargeMailPrototype : BlankMailPrototype;
// End Frontier
if (!EntityManager.HasComponent<MailReceiverComponent>(recipientUid))
var mailSystem = _entitySystemManager.GetEntitySystem<MailSystem>();
var containerSystem = _entitySystemManager.GetEntitySystem<SharedContainerSystem>();
if (!_entityManager.HasComponent<MailReceiverComponent>(recipientUid))
{
shell.WriteLine(Loc.GetString("cmd-mailto-no-mailreceiver", ("requiredComponent", nameof(MailReceiverComponent))));
shell.WriteLine(Loc.GetString("command-mailto-no-mailreceiver", ("requiredComponent", nameof(MailReceiverComponent))));
return;
}
if (!_prototype.HasIndex<EntityPrototype>(mailPrototype))
if (!_prototypeManager.HasIndex<EntityPrototype>(mailPrototype)) // Frontier: _blankMailPrototype<mailPrototype
{
shell.WriteLine(Loc.GetString("cmd-mailto-no-blankmail", ("blankMail", mailPrototype)));
shell.WriteLine(Loc.GetString("command-mailto-no-blankmail", ("blankMail", mailPrototype))); // Frontier: _blankMailPrototype<mailPrototype
return;
}
if (!_container.TryGetContainer(containerUid, Container, out var targetContainer))
if (!containerSystem.TryGetContainer(containerUid, Container, out var targetContainer))
{
shell.WriteLine(Loc.GetString("cmd-mailto-invalid-container", ("requiredContainer", Container)));
shell.WriteLine(Loc.GetString("command-mailto-invalid-container", ("requiredContainer", Container)));
return;
}
if (!_sharedMail.TryGetMailRecipientForReceiver(recipientUid, out var recipient))
if (!mailSystem.TryGetMailRecipientForReceiver(recipientUid, out var recipient))
{
shell.WriteLine(Loc.GetString("cmd-mailto-unable-to-receive"));
shell.WriteLine(Loc.GetString("command-mailto-unable-to-receive"));
return;
}
if (!_sharedMail.TryGetMailTeleporterForReceiver(recipientUid, out var teleporterComponent, out var teleporterUid))
if (!mailSystem.TryGetMailTeleporterForReceiver(recipientUid, out var teleporterComponent, out var teleporterUid))
{
shell.WriteLine(Loc.GetString("cmd-mailto-no-teleporter-found"));
shell.WriteLine(Loc.GetString("command-mailto-no-teleporter-found"));
return;
}
var mailUid = EntityManager.SpawnEntity(mailPrototype, EntityManager.GetComponent<TransformComponent>(containerUid).Coordinates);
var mailContents = _container.EnsureContainer<Container>(mailUid, MailContainer);
var mailUid = _entityManager.SpawnEntity(mailPrototype, _entityManager.GetComponent<TransformComponent>(containerUid).Coordinates); // Frontier: _blankMailPrototype<mailPrototype
var mailContents = containerSystem.EnsureContainer<Container>(mailUid, MailContainer);
if (!EntityManager.TryGetComponent<MailComponent>(mailUid, out var mailComponent))
if (!_entityManager.TryGetComponent<MailComponent>(mailUid, out var mailComponent))
{
shell.WriteLine(Loc.GetString("cmd-mailto-bogus-mail", ("blankMail", mailPrototype), ("requiredMailComponent", nameof(MailComponent))));
shell.WriteLine(Loc.GetString("command-mailto-bogus-mail", ("blankMail", mailPrototype), ("requiredMailComponent", nameof(MailComponent)))); // Frontier: _blankMailPrototype<mailPrototype
return;
}
foreach (var entity in targetContainer.ContainedEntities.ToArray())
{
_container.Insert(entity, mailContents);
containerSystem.Insert(entity, mailContents);
}
_sharedMail.SetFragile((mailUid, mailComponent), isFragile);
_sharedMail.SetPriority((mailUid, mailComponent), isPriority);
_sharedMail.SetLarge((mailUid, mailComponent), isLarge);
mailComponent.IsFragile = isFragile;
mailComponent.IsPriority = isPriority;
mailComponent.IsLarge = isLarge; //Frontier Mail
_mail.SetupMail(mailUid, teleporterComponent, recipient.Value);
mailSystem.SetupMail(mailUid, teleporterComponent, recipient.Value);
var teleporterQueue = _container.EnsureContainer<Container>((EntityUid)teleporterUid, "queued");
_container.Insert(mailUid, teleporterQueue);
shell.WriteLine(Loc.GetString("cmd-mailto-success", ("timeToTeleport", teleporterComponent.NextDelivery - _timing.CurTime)));
var teleporterQueue = containerSystem.EnsureContainer<Container>((EntityUid)teleporterUid, "queued");
containerSystem.Insert(mailUid, teleporterQueue);
shell.WriteLine(Loc.GetString("command-mailto-success", ("timeToTeleport", teleporterComponent.NextDelivery - _timing.CurTime)));
}
}
[AdminCommand(AdminFlags.Fun)]
public sealed class MailNowCommand : LocalizedEntityCommands
public sealed class MailNowCommand : IConsoleCommand
{
public override string Command => "mailnow";
public override string Description => Loc.GetString("cmd-mailnow");
public override string Help => Loc.GetString("cmd-mailnow-help", ("command", Command));
public string Command => "mailnow";
public string Description => Loc.GetString("command-mailnow");
public string Help => Loc.GetString("command-mailnow-help", ("command", Command));
[Dependency] private readonly MailSystem _mail = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
var query = EntityManager.EntityQueryEnumerator<MailTeleporterComponent>();
while (query.MoveNext(out var uid, out var mailTeleporter))
foreach (var mailTeleporter in _entityManager.EntityQuery<MailTeleporterComponent>())
{
_mail.DeliverNow((uid, mailTeleporter));
mailTeleporter.NextDelivery = _timing.CurTime;
}
shell.WriteLine(Loc.GetString("cmd-mailnow-success"));
shell.WriteLine(Loc.GetString("command-mailnow-success"));
}
}
@@ -6,21 +6,21 @@ namespace Content.Server.Roboisseur.Roboisseur
public sealed partial class RoboisseurComponent : Component
{
[ViewVariables]
[DataField]
[DataField("accumulator")]
public float Accumulator = 0f;
[ViewVariables(VVAccess.ReadOnly)]
[DataField]
[DataField("impatient")]
public Boolean Impatient { get; set; } = false;
[ViewVariables]
[DataField]
[DataField("resetTime")]
public TimeSpan ResetTime = TimeSpan.FromMinutes(10);
[DataField]
[DataField("barkAccumulator")]
public float BarkAccumulator = 0f;
[DataField]
[DataField("barkTime")]
public TimeSpan BarkTime = TimeSpan.FromMinutes(1);
/// <summary>
@@ -28,7 +28,7 @@ namespace Content.Server.Roboisseur.Roboisseur
/// </summary>
public TimeSpan StateTime = default!;
[DataField]
[DataField("stateCD")]
public TimeSpan StateCD = TimeSpan.FromSeconds(5);
[ViewVariables(VVAccess.ReadWrite)]
@@ -21,6 +21,7 @@ namespace Content.Server.Roboisseur.Roboisseur
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
@@ -1,9 +1,7 @@
using Content.Server._DV.Objectives.Systems;
using Content.Server.Objectives.Systems;
using Content.Shared.Roles;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// Requires that the player not have a certain job to have this objective.
/// </summary>
@@ -1,8 +1,8 @@
using Content.Server._DV.Objectives.Components;
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Components;
namespace Content.Server._DV.Objectives.Systems;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles checking the job blacklist for this objective.
@@ -13,7 +13,6 @@ public enum AutomaticSpareIdState
AwaitingUnlock,
Unlocked,
CaptainPresent,
WarOps
}
[RegisterComponent, Access(typeof(AutomaticSpareIdSystem)), AutoGenerateComponentPause]
@@ -39,17 +38,9 @@ public sealed partial class AutomaticSpareIdComponent : Component
/// <summary>
/// The access that the spare ID safe will be extended to have if it is automatically unlocked
/// if there is no captain
/// </summary>
[DataField]
public ProtoId<AccessLevelPrototype> GrantAccessToCommand = "Command";
/// <summary>
/// The access that the spare ID safe will be extended to have if it is automatically unlocked
/// if there is a captain
/// </summary>
[DataField]
public ProtoId<AccessLevelPrototype> GrantAccessToCaptain = "Captain";
public ProtoId<AccessLevelPrototype> GrantAccessTo = "Command";
/// <summary>
/// Message for when a Captain joins after the system has alerted about their absence
@@ -74,19 +65,4 @@ public sealed partial class AutomaticSpareIdComponent : Component
/// </summary>
[DataField]
public LocId UnlockedMessage = "no-captain-aa-unlocked-announcement";
/// <summary>
/// The amount of time in which that the spare ID will unlock after nuclear operatives declare war.
/// </summary>
public TimeSpan WarOpsUnlockDelay = TimeSpan.FromSeconds(15);
/// <summary>
/// Message that will be displayed to the station when there is no captain and war ops is declared.
/// </summary>
public LocId WarOpsUnlockedMessageACO = "spare-id-warops-no-captain";
/// <summary>
/// Message that will be displayed to the station when there is a captain and war ops is declared.
/// </summary>
public LocId WarOpsUnlockedMessageCaptain = "spare-id-warops-captain";
}
@@ -3,16 +3,13 @@ using Content.Server._DV.Cabinet;
using Content.Server._DV.Station.Components;
using Content.Server._DV.Station.Events;
using Content.Server.Chat.Systems;
using Content.Server.NukeOps;
using Content.Server.Station.Components;
using Content.Shared._DV.CCVars;
using Content.Shared.Access.Components;
using Content.Shared.Access;
using Content.Shared.NukeOps;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Station.Systems;
@@ -33,7 +30,6 @@ public sealed class AutomaticSpareIdSystem : EntitySystem
SubscribeLocalEvent<AutomaticSpareIdComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AutomaticSpareIdComponent, PlayerJobAddedEvent>(OnPlayerJobAdded);
SubscribeLocalEvent<AutomaticSpareIdComponent, PlayerJobsRemovedEvent>(OnPlayerJobsRemoved);
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
Subs.CVar(_cfg, DCCVars.SpareIdAutoUnlock, a => _autoUnlock = a, true);
Subs.CVar(_cfg, DCCVars.SpareIdAlertDelay, a => _alertDelay = a, true);
@@ -67,19 +63,6 @@ public sealed class AutomaticSpareIdSystem : EntitySystem
{
MoveToUnlocked(ent);
}
else if (ent.Comp.State is AutomaticSpareIdState.WarOps)
{
// Default to these, then check if there is a captain
var message = ent.Comp.WarOpsUnlockedMessageACO;
var accessGranted = ent.Comp.GrantAccessToCommand;
if (HasCaptain(ent))
{
message = ent.Comp.WarOpsUnlockedMessageCaptain;
accessGranted = ent.Comp.GrantAccessToCaptain;
}
ent.Comp.State = AutomaticSpareIdState.AwaitingUnlock;
MoveToUnlocked(ent, accessGranted, message);
}
else
{
DebugTools.Assert($"Spare ID state timed out with unexpected state {ent.Comp.State}");
@@ -94,7 +77,7 @@ public sealed class AutomaticSpareIdSystem : EntitySystem
MoveToAlerted(ent);
}
private static void RoundStartCaptain(Entity<AutomaticSpareIdComponent> ent)
private void RoundStartCaptain(Entity<AutomaticSpareIdComponent> ent)
{
ent.Comp.State = AutomaticSpareIdState.CaptainPresent;
ent.Comp.Timeout = null;
@@ -119,18 +102,6 @@ public sealed class AutomaticSpareIdSystem : EntitySystem
MoveToAlerted(ent);
}
private void OnWarDeclared(ref WarDeclaredEvent args)
{
if (args.Status == WarConditionStatus.YesWar)
{
foreach (var spareId in EntityQuery<AutomaticSpareIdComponent>())
{
spareId.Timeout = _timing.CurTime + spareId.WarOpsUnlockDelay;
spareId.State = AutomaticSpareIdState.WarOps;
}
}
}
private bool HasCaptain(Entity<AutomaticSpareIdComponent> ent)
{
if (!TryComp<StationJobsComponent>(ent, out var stationJobs))
@@ -149,13 +120,7 @@ public sealed class AutomaticSpareIdSystem : EntitySystem
_chat.DispatchStationAnnouncement(ent, Loc.GetString(ent.Comp.AwaitingUnlockMessage, ("minutes", _unlockDelay.TotalMinutes)), colorOverride: Color.Gold);
}
/// <summary>
/// Unlocks all spare ID cabinets, giving access to a certain access prototype and displays a message to the station.
/// </summary>
/// <param name="ent">The station entity that has the <see cref="AutomaticSpareIdComponent"/>.</param>
/// <param name="newSpareIdAccess">The access to give to the spare ID cabinet. If not specified or null, will default to ent.Comp.GrantAccessToCommand</param>
/// <param name="unlockMessageLocId">The message to display to the station upon unlocking the spare ID. If not specified or null, will default to ent.Comp.UnlockedMessage</param>
private void MoveToUnlocked(Entity<AutomaticSpareIdComponent> ent, ProtoId<AccessLevelPrototype>? newSpareIdAccess = null, LocId? unlockMessageLocId = null)
private void MoveToUnlocked(Entity<AutomaticSpareIdComponent> ent)
{
DebugTools.Assert(ent.Comp.State is AutomaticSpareIdState.AwaitingUnlock, $"Spare ID state has unexpected state {ent.Comp.State} on unlocking");
@@ -169,18 +134,12 @@ public sealed class AutomaticSpareIdSystem : EntitySystem
if (accesses.Count <= 0)
continue;
if (!newSpareIdAccess.HasValue)
newSpareIdAccess = ent.Comp.GrantAccessToCommand; // Default to command if no access is specified
accesses.Add([newSpareIdAccess.Value]);
accesses.Add([ent.Comp.GrantAccessTo]);
Dirty(uid, accessReader);
RaiseLocalEvent(uid, new AccessReaderConfigurationChangedEvent());
}
if (!unlockMessageLocId.HasValue)
unlockMessageLocId = ent.Comp.UnlockedMessage; // Default message if nothing is specified
_chat.DispatchStationAnnouncement(ent, Loc.GetString(unlockMessageLocId), colorOverride: Color.Red);
_chat.DispatchStationAnnouncement(ent, Loc.GetString(ent.Comp.UnlockedMessage), colorOverride: Color.Red);
}
private void MoveToCaptainPresent(Entity<AutomaticSpareIdComponent> ent)
@@ -1,43 +0,0 @@
using Content.Server._DV.StationEvents.Events;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.StationEvents.Components;
/// <summary>
/// Spawns a small amount of randomly-colored foxfires,
/// centered around either the Oracle or Sophic Grammateus.
/// </summary>
[RegisterComponent, Access(typeof(GlimmerFoxfireSpawnRule))]
public sealed partial class GlimmerFoxfireSpawnRuleComponent : Component
{
/// <summary>
/// Minimum+ amounts of foxfires to spawn.
/// </summary>
[DataField]
public int MinimumSpawned = 10;
/// <summary>
/// Maximum amounts of foxfires to spawn.
/// </summary>
[DataField]
public int MaximumSpawned = 20;
/// <summary>
/// Maximum distance from the Oracle or Sophic Grammateus in which the
/// foxfire will be spawned.
/// </summary>
[DataField]
public int SpawnRange = 10;
/// <summary>
/// Prototype to be spawned.
/// </summary>
[DataField]
public EntProtoId FoxfirePrototype = "Foxfire";
/// <summary>
/// Available colors for the foxfire's light.
/// </summary>
[DataField]
public List<Color>? RandomColorList = new();
}
@@ -1,35 +0,0 @@
using Content.Server._DV.StationEvents.Events;
namespace Content.Server._DV.StationEvents.Components;
/// <summary>
/// Attempts to change the hair and facial hair markings, plus their color
/// of a small amount of people.
/// </summary>
[RegisterComponent, Access(typeof(GlimmerRestyleRule))]
public sealed partial class GlimmerRestyleRuleComponent : Component
{
/// <summary>
/// Minimum number of valid targets that will get restyled.
/// </summary>
[DataField]
public int MinimumTargets = 1;
/// <summary>
/// Maximum number of valid targets that will get restyled.
/// </summary>
[DataField]
public int MaximumTargets = 5;
/// <summary>
/// Chance of completely removing all hair markings instead of selecting a random one.
/// </summary>
[DataField]
public float BaldChance = 0.2f;
/// <summary>
/// Chance of completely removing all facial hair markings instead of selecting a random one.
/// </summary>
[DataField]
public float CleanShavenChance = 0.5f;
}
@@ -16,46 +16,46 @@ public sealed partial class MeteorSwarmRuleComponent : Component
[DataField("waves")]
public int? WaveCounter = null;
[DataField]
[DataField("minimumWaves")]
public int MinimumWaves = 3;
[DataField]
[DataField("maximumWaves")]
public int MaximumWaves = 8;
[DataField]
[DataField("minimumCooldown")]
public float MinimumCooldown = 10f;
[DataField]
[DataField("maximumCooldown")]
public float MaximumCooldown = 60f;
[DataField]
[DataField("meteorsPerWave")]
public int MeteorsPerWave = 5;
[DataField]
[DataField("meteorVelocity")]
public float MeteorVelocity = 10f;
[DataField]
[DataField("maxAngularVelocity")]
public float MaxAngularVelocity = 2f;
[DataField]
[DataField("minAngularVelocity")]
public float MinAngularVelocity = -2f;
/// <summary>
/// Stagger the spawns a bit, but not too much so they still come in waves
/// </summary>
[DataField]
[DataField("spawnDistanceVariation")]
public float SpawnDistanceVariation = 50f;
/// <summary>
/// Percentage of the station area to target. Allow for some near-miss fly-by meteors to jump-scare crew on EVAs.
/// Stations aren't perfect rectangles like the targeting area, so even 1.0 will still have some near-misses.
/// </summary>
[DataField]
[DataField("targetingSpread")]
public float TargetingSpread = 1.05f;
/// <summary>
/// How long before a meteor despawns (in case it missed everything).
/// </summary>
[DataField]
[DataField("meteorLifetime")]
public float MeteorLifetime = 300f;
}
@@ -10,15 +10,12 @@ public sealed partial class MinorMassMindSwapRuleComponent : Component
/// <summary>
/// The mind swap is only temporary if true.
/// </summary>
[DataField]
[DataField("isTemporary")]
public bool IsTemporary = false;
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(60);
[DataField]
public int ReturnSwapCooldown = 120;
[DataField]
public SoundSpecifier AnnouncementSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
@@ -108,35 +108,35 @@ public sealed class FugitiveRule : StationEventSystem<FugitiveRuleComponent>
private FormattedMessage GenerateReport(EntityUid uid, FugitiveRuleComponent rule)
{
var report = new FormattedMessage();
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-title"));
report.PushMarkup(Loc.GetString("fugitive-report-title"));
report.PushNewline();
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-first-line"));
report.PushMarkup(Loc.GetString("fugitive-report-first-line"));
report.PushNewline();
if (!TryComp<HumanoidAppearanceComponent>(uid, out var humanoid))
{
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-inhuman", ("name", uid)));
report.AddMarkup(Loc.GetString("fugitive-report-inhuman", ("name", uid)));
return report;
}
var species = PrototypeManager.Index(humanoid.Species);
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-morphotype", ("species", Loc.GetString(species.Name))));
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-age", ("age", humanoid.Age)));
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-sex", ("sex", humanoid.Sex.ToString())));
report.PushMarkup(Loc.GetString("fugitive-report-morphotype", ("species", Loc.GetString(species.Name))));
report.PushMarkup(Loc.GetString("fugitive-report-age", ("age", humanoid.Age)));
report.PushMarkup(Loc.GetString("fugitive-report-sex", ("sex", humanoid.Sex.ToString())));
if (TryComp<PhysicsComponent>(uid, out var physics))
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-weight", ("weight", Math.Round(physics.FixturesMass))));
report.PushMarkup(Loc.GetString("fugitive-report-weight", ("weight", Math.Round(physics.FixturesMass))));
// add a random identifying quality that officers can use to track them down
report.AddMarkupOrThrow(RobustRandom.Next(0, 2) switch
report.PushMarkup(RobustRandom.Next(0, 2) switch
{
0 => Loc.GetString("fugitive-report-detail-dna", ("dna", GetDNA(uid))),
_ => Loc.GetString("fugitive-report-detail-prints", ("prints", GetPrints(uid)))
});
report.PushNewline();
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-crimes-header"));
report.PushMarkup(Loc.GetString("fugitive-report-crimes-header"));
// generate some random crimes to avoid this situation
// "officer what are my charges?"
@@ -144,7 +144,7 @@ public sealed class FugitiveRule : StationEventSystem<FugitiveRuleComponent>
AddCharges(report, rule);
report.PushNewline();
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-last-line"));
report.AddMarkup(Loc.GetString("fugitive-report-last-line"));
return report;
}
@@ -172,7 +172,7 @@ public sealed class FugitiveRule : StationEventSystem<FugitiveRuleComponent>
foreach (var crime in crimes)
{
var count = RobustRandom.Next(rule.MinCounts, rule.MaxCounts + 1);
report.AddMarkupOrThrow(Loc.GetString("fugitive-report-crime", ("crime", Loc.GetString(crime)), ("count", count)));
report.PushMarkup(Loc.GetString("fugitive-report-crime", ("crime", Loc.GetString(crime)), ("count", count)));
}
}
}
@@ -1,59 +0,0 @@
using System.Numerics;
using Content.Server._DV.StationEvents.Components;
using Content.Server.Nyanotrasen.Research.SophicScribe;
using Content.Server.Research.Oracle;
using Content.Server.StationEvents.Events;
using Content.Shared.GameTicking.Components;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server._DV.StationEvents.Events;
public sealed class GlimmerFoxfireSpawnRule : StationEventSystem<GlimmerFoxfireSpawnRuleComponent>
{
[Dependency] private readonly SharedPointLightSystem _light = default!;
protected override void Started(EntityUid uid,
GlimmerFoxfireSpawnRuleComponent comp,
GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
base.Started(uid, comp, gameRule, args);
var locations = new List<EntityCoordinates>();
var queryScribe = EntityQueryEnumerator<SophicScribeComponent, TransformComponent>();
var queryOracle = EntityQueryEnumerator<OracleComponent, TransformComponent>();
while (queryScribe.MoveNext(out _, out var transform))
{
locations.Add(transform.Coordinates);
}
while (queryOracle.MoveNext(out _, out var transform))
{
locations.Add(transform.Coordinates);
}
if (locations.Count == 0)
return;
var selectedLocation = RobustRandom.Pick(locations);
var amountToSpawn = RobustRandom.Next(comp.MinimumSpawned, comp.MaximumSpawned);
for (var i = 0; i < amountToSpawn; i++)
{
var spawnLocation = selectedLocation.Offset(new Vector2(
RobustRandom.Next(-comp.SpawnRange, comp.SpawnRange),
RobustRandom.Next(-comp.SpawnRange, comp.SpawnRange)
));
var color = Color.GhostWhite;
if (comp.RandomColorList != null && comp.RandomColorList.Count != 0)
color = RobustRandom.Pick(comp.RandomColorList);
var fireEnt = Spawn(comp.FoxfirePrototype, spawnLocation);
_light.SetColor(fireEnt, color);
}
}
}
@@ -1,76 +0,0 @@
using System.Linq;
using Content.Server._DV.StationEvents.Components;
using Content.Server.Psionics;
using Content.Server.StationEvents.Events;
using Content.Shared.Abilities.Psionics;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Mobs.Components;
using Content.Shared.SSDIndicator;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Robust.Shared.Random;
namespace Content.Server._DV.StationEvents.Events;
public sealed class GlimmerRestyleRule : StationEventSystem<GlimmerRestyleRuleComponent>
{
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MarkingManager _markingManager = default!;
protected override void Started(EntityUid uid, GlimmerRestyleRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, comp, gameRule, args);
var query = EntityQueryEnumerator<HumanoidAppearanceComponent, MobStateComponent>();
List<(EntityUid, HumanoidAppearanceComponent)> potentialTargets = new();
while (query.MoveNext(out var entity, out var humanoid, out var mobState))
{
if (!_mob.IsAlive(entity, mobState) || HasComp<PsionicInsulationComponent>(entity) || !HasComp<PotentialPsionicComponent>(entity))
continue;
potentialTargets.Add((entity, humanoid));
}
_random.Shuffle(potentialTargets);
var targetsToRestyle = _random.Next(comp.MinimumTargets, comp.MaximumTargets);
foreach (var (entity, humanoid) in potentialTargets)
{
if(HasComp<SSDIndicatorComponent>(entity))
continue;
if (targetsToRestyle-- <= 0)
break;
var changedHair = TryApplyRestyle((entity, humanoid), MarkingCategories.Hair, comp.BaldChance);
var changedFacialHair = TryApplyRestyle((entity, humanoid), MarkingCategories.FacialHair, comp.CleanShavenChance);
if (changedHair || changedFacialHair)
_popup.PopupEntity(Loc.GetString("glimmer-restyle-event"), entity, entity, PopupType.Medium);
Dirty(entity, humanoid);
}
}
private bool TryApplyRestyle(Entity<HumanoidAppearanceComponent> ent, MarkingCategories category, float noMarkingsChance)
{
var newMarkingColor = new Color(_random.NextFloat(), _random.NextFloat(), _random.NextFloat());
var availableMarkings = _markingManager.MarkingsByCategoryAndSpecies(category, ent.Comp.Species);
if (availableMarkings.Count == 0)
return false;
var hadCategoryBefore = ent.Comp.MarkingSet.TryGetCategory(category, out _);
ent.Comp.MarkingSet.RemoveCategory(category);
if (_random.Prob(noMarkingsChance))
return hadCategoryBefore; //Do not show the popup if you go from no markings to no markings.
var newMarking = _random.Pick(availableMarkings.Values.ToList()).AsMarking();
newMarking.SetColor(newMarkingColor);
ent.Comp.MarkingSet.AddCategory(category);
ent.Comp.MarkingSet.AddFront(category, newMarking);
return true;
}
}
@@ -95,7 +95,7 @@ internal sealed class MinorMassMindSwapRule : StationEventSystem<MinorMassMindSw
var target01 = _random.PickAndTake(psionicActors);
var target02 = _random.PickAndTake(psionicActors);
_mindSwap.Swap(target01, target02, false, component.ReturnSwapCooldown);
_mindSwap.Swap(target01, target02);
if (!component.IsTemporary)
{
@@ -20,12 +20,13 @@ public sealed class PryingRequiresPowerSystem : EntitySystem
private void OnPried(Entity<PryingRequiresPowerComponent> ent, ref PriedEvent args)
{
// Entity has a PowerCellSlot, try that first
if (_cell.TryUseCharge(ent.Owner, ent.Comp.PowerCost))
// Tool is a battery use its power
if (_battery.TryUseCharge(ent.Owner, ent.Comp.PowerCost))
return;
// The entity itself is a battery
_battery.TryUseCharge(ent.Owner, ent.Comp.PowerCost);
// User has a battery
if (_battery.TryUseCharge(ent.Owner, ent.Comp.PowerCost))
return;
}
private void OnBeforePry(Entity<PryingRequiresPowerComponent> ent, ref BeforePryEvent args)
@@ -16,14 +16,14 @@ public sealed partial class EnergyGunComponent : Component
/// <summary>
/// A list of the different firing modes the energy gun can switch between
/// </summary>
[DataField(required: true)]
[DataField("fireModes", required: true)]
[AutoNetworkedField]
public List<EnergyWeaponFireMode> FireModes = new();
/// <summary>
/// The currently selected firing mode
/// </summary>
[DataField]
[DataField("currentFireMode")]
[AutoNetworkedField]
public EnergyWeaponFireMode? CurrentFireMode = default!;
}
@@ -40,18 +40,18 @@ public sealed partial class EnergyWeaponFireMode
/// <summary>
/// The battery cost to fire the projectile associated with this firing mode
/// </summary>
[DataField]
[DataField("fireCost")]
public float FireCost = 100;
/// <summary>
/// The name of the selected firemode
/// </summary>
[DataField]
[DataField("name")]
public string Name = string.Empty;
/// <summary>
/// What RsiState we use for that firemode if it needs to change.
/// </summary>
[DataField]
[DataField("state")]
public string State = string.Empty;
}
@@ -4,7 +4,6 @@ using Content.Server.Psionics;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
namespace Content.Server._DV.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class PsionicProducingArtifactSystem : EntitySystem
{
[Dependency] private readonly SharedXenoArtifactSystem _artifact = default!;
@@ -32,6 +31,8 @@ public sealed class PsionicProducingArtifactSystem : EntitySystem
// Pick first active node
var node = _artifact.GetActiveNodes(artifactEntity).FirstOrDefault();
if (node == null)
return;
// Track psionic usage using ConsumedResearchValue
var currentAmount = _artifact.GetResearchValue(node);
@@ -9,6 +9,8 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
public sealed class ArtifactMetapsionicTriggerSystem : BaseXATSystem<ArtifactMetapsionicTriggerComponent>
{
[Dependency] private readonly XenoArtifactSystem _artifact = default!;
private EntityQuery<XenoArtifactComponent> _xenoArtifactQuery;
public override void Initialize()
@@ -0,0 +1,127 @@
using Content.Server.Atmos.Components;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
// using Content.Shared.Standing;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server._EE.FootPrint;
public sealed class FootPrintsSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
private EntityQuery<TransformComponent> _transformQuery;
private EntityQuery<MobThresholdsComponent> _mobThresholdQuery;
private EntityQuery<AppearanceComponent> _appearanceQuery;
// private EntityQuery<LayingDownComponent> _layingQuery;
public override void Initialize()
{
base.Initialize();
_transformQuery = GetEntityQuery<TransformComponent>();
_mobThresholdQuery = GetEntityQuery<MobThresholdsComponent>();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
// _layingQuery = GetEntityQuery<LayingDownComponent>();
SubscribeLocalEvent<FootPrintsComponent, ComponentStartup>(OnStartupComponent);
SubscribeLocalEvent<FootPrintsComponent, MoveEvent>(OnMove);
}
private void OnStartupComponent(EntityUid uid, FootPrintsComponent component, ComponentStartup args)
{
component.StepSize = Math.Max(0f, component.StepSize + _random.NextFloat(-0.05f, 0.05f));
}
private void OnMove(EntityUid uid, FootPrintsComponent component, ref MoveEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (component.PrintsColor.A <= 0f
|| !_transformQuery.TryComp(uid, out var transform)
|| !_mobThresholdQuery.TryComp(uid, out var mobThreshHolds)
|| !_map.TryFindGridAt(_transform.GetMapCoordinates((uid, transform)), out var gridUid, out _))
return;
var dragging = mobThreshHolds.CurrentThresholdState is MobState.Critical or MobState.Dead;
var distance = (transform.LocalPosition - component.StepPos).Length();
var stepSize = dragging ? component.DragSize : component.StepSize;
if (!(distance > stepSize))
return;
component.RightStep = !component.RightStep;
var entity = Spawn(component.StepProtoId, CalcCoords(gridUid, component, transform, dragging));
var footPrintComponent = EnsureComp<FootPrintComponent>(entity);
footPrintComponent.PrintOwner = uid;
Dirty(entity, footPrintComponent);
if (_appearanceQuery.TryComp(entity, out var appearance))
{
_appearance.SetData(entity, FootPrintVisualState.State, PickState(uid, dragging), appearance);
_appearance.SetData(entity, FootPrintVisualState.Color, component.PrintsColor, appearance);
}
if (!_transformQuery.TryComp(entity, out var stepTransform))
return;
stepTransform.LocalRotation = dragging
? (transform.LocalPosition - component.StepPos).ToAngle() + Angle.FromDegrees(-90f)
: transform.LocalRotation + Angle.FromDegrees(180f);
component.PrintsColor = component.PrintsColor.WithAlpha(Math.Max(0f, component.PrintsColor.A - component.ColorReduceAlpha));
component.StepPos = transform.LocalPosition;
if (!TryComp<SolutionContainerManagerComponent>(entity, out var solutionContainer)
|| !_solution.ResolveSolution((entity, solutionContainer), footPrintComponent.SolutionName, ref footPrintComponent.Solution, out var solution)
|| string.IsNullOrWhiteSpace(component.ReagentToTransfer) || solution.Volume >= 1)
return;
_solution.TryAddReagent(footPrintComponent.Solution.Value, component.ReagentToTransfer, 0.01, out _); //was 1
}
private EntityCoordinates CalcCoords(EntityUid uid, FootPrintsComponent component, TransformComponent transform, bool state)
{
if (state)
return new EntityCoordinates(uid, transform.LocalPosition);
var offset = component.RightStep
? new Angle(Angle.FromDegrees(180f) + transform.LocalRotation).RotateVec(component.OffsetPrint)
: new Angle(transform.LocalRotation).RotateVec(component.OffsetPrint);
return new EntityCoordinates(uid, transform.LocalPosition + offset);
}
private FootPrintVisuals PickState(EntityUid uid, bool dragging)
{
var state = FootPrintVisuals.BareFootPrint;
if (_inventory.TryGetSlotEntity(uid, "shoes", out _))
state = FootPrintVisuals.ShoesPrint;
if (_inventory.TryGetSlotEntity(uid, "outerClothing", out var suit) && TryComp<PressureProtectionComponent>(suit, out _))
state = FootPrintVisuals.SuitPrint;
if (dragging)
state = FootPrintVisuals.Dragging;
return state;
}
}
@@ -0,0 +1,57 @@
using System.Linq;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Fluids;
using Content.Shared.Fluids.Components;
using Robust.Shared.Physics.Events;
namespace Content.Server._EE.FootPrint;
public sealed class PuddleFootPrintsSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PuddleFootPrintsComponent, EndCollideEvent>(OnStepTrigger);
}
private void OnStepTrigger(EntityUid uid, PuddleFootPrintsComponent component, ref EndCollideEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (!TryComp<AppearanceComponent>(uid, out var appearance)
|| !TryComp<PuddleComponent>(uid, out var puddle)
|| !TryComp<FootPrintsComponent>(args.OtherEntity, out var tripper)
|| !TryComp<SolutionContainerManagerComponent>(uid, out var solutionManager)
|| !_solutionContainer.ResolveSolution((uid, solutionManager), puddle.SolutionName, ref puddle.Solution, out var solutions))
return;
var totalSolutionQuantity = solutions.Contents.Sum(sol => (float) sol.Quantity);
var waterQuantity = (from sol in solutions.Contents where sol.Reagent.Prototype == "Water" select (float) sol.Quantity).FirstOrDefault();
if (waterQuantity / (totalSolutionQuantity / 100f) > component.OffPercent || solutions.Contents.Count <= 0)
return;
tripper.ReagentToTransfer =
solutions.Contents.Aggregate((l, r) => l.Quantity > r.Quantity ? l : r).Reagent.Prototype;
if (_appearance.TryGetData(uid, PuddleVisuals.SolutionColor, out var color, appearance)
&& _appearance.TryGetData(uid, PuddleVisuals.CurrentVolume, out var volume, appearance))
AddColor((Color) color, (float) volume * component.SizeRatio, tripper);
_solutionContainer.RemoveEachReagent(puddle.Solution.Value, 0.01); //was 1
}
private void AddColor(Color col, float quantity, FootPrintsComponent component)
{
component.PrintsColor = component.ColorQuantity == 0f ? col : Color.InterpolateBetween(component.PrintsColor, col, component.ColorInterpolationFactor);
component.ColorQuantity += quantity;
}
}
@@ -43,9 +43,6 @@ public sealed partial class IdCardConsoleComponent : Component
[DataField, AutoNetworkedField]
public List<ProtoId<AccessLevelPrototype>> AccessLevels = new()
{
// DeltaV note: make sure any additions to this list are also added to both:
// 1. AllAccess in Resources\Prototypes\Access\misc.yml
// 2. ComputerIdAdmeme in Resources\Prototypes\_DV\Entities\Structures\Machines\computers.yml
"Armory",
"Atmospherics",
"Bar",
@@ -62,11 +59,8 @@ public sealed partial class IdCardConsoleComponent : Component
"Corpsman", // DeltaV - Add Corpsman access
"Command",
"Cryogenics",
"EmergencyShuttleRepealAll", // DeltaV - fix mismatch with Access/misc.yml
"Engineering",
"External",
"GenpopEnter",
"GenpopLeave",
"HeadOfPersonnel",
"HeadOfSecurity",
"Hydroponics",
@@ -1,5 +1,4 @@
using Robust.Shared.GameStates;
using Content.Shared.Guidebook;
namespace Content.Shared.Atmos.Piping.Binary.Components;
@@ -34,7 +33,6 @@ public sealed partial class GasVolumePumpComponent : Component
public float LowerThreshold = 0.01f;
[DataField]
[GuidebookData]
public float HigherThreshold = DefaultHigherThreshold;
public static readonly float DefaultHigherThreshold = 2 * Atmospherics.MaxOutputPressure;
@@ -258,8 +258,9 @@ public partial class SharedBodySystem
{
if (id is null
|| !Resolve(id.Value, ref body, logMissing: false)
|| body.RootContainer is null // Delta-V - Fix this shitmed change
|| body.RootContainer.ContainedEntity is null
|| body is null // Shitmed Change
|| body.RootContainer == default // Shitmed Change
|| !Resolve(body.RootContainer.ContainedEntity.Value, ref rootPart))
{
yield break;
@@ -65,11 +65,6 @@ namespace Content.Shared.Humanoid.Markings
var markingPoints = _prototypeManager.Index(speciesProto.MarkingPoints);
var res = new Dictionary<string, MarkingPrototype>();
// Begin DeltaV addition - prevents errors when category is missing
if (!markingPoints.Points.ContainsKey(category))
return res;
// End DeltaV addition
foreach (var (key, marking) in MarkingsByCategory(category))
{
if ((markingPoints.OnlyWhitelisted || markingPoints.Points[category].OnlyWhitelisted) && marking.SpeciesRestrictions == null)
@@ -46,12 +46,6 @@ namespace Content.Shared.Kitchen
[DataField]
public bool SecretRecipe = false;
/// <summary>
/// DeltaV: Changes the guidebook formatting to "Fry for"
/// </summary>
[DataField]
public bool DeepFried;
/// <summary>
/// Count the number of ingredients in a recipe for sorting the recipe list.
/// This makes sure that where ingredient lists overlap, the more complex
@@ -15,7 +15,7 @@ public sealed partial class MouseRotatorComponent : Component
/// How much the desired angle needs to change before a predictive event is sent
/// </summary>
[DataField, AutoNetworkedField]
public Angle AngleTolerance = Angle.FromDegrees(5.0); // DeltaV - smooth flashlights, was 20
public Angle AngleTolerance = Angle.FromDegrees(20.0);
/// <summary>
/// The angle that will be lerped to
@@ -37,7 +37,7 @@ public sealed partial class MouseRotatorComponent : Component
/// like turrets or ship guns, which have finer range of movement.
/// </summary>
[DataField, AutoNetworkedField]
public bool Simple4DirMode; // DeltaV - smooth flashlights, was true
public bool Simple4DirMode = true;
}
/// <summary>
@@ -3,15 +3,13 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Nutrition.Components;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState] // DV - Deep Fryers
public sealed partial class FlavorProfileComponent : Component
{
/// <summary>
/// Localized string containing the base flavor of this entity.
/// </summary>
[DataField]
[AutoNetworkedField] // DV - Deep Fryers
public HashSet<string> Flavors = new(); // DV remove setter
public HashSet<string> Flavors { get; private set; } = new();
/// <summary>
/// Reagent IDs to ignore when processing this flavor profile. Defaults to nutriment.
@@ -0,0 +1,29 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen
{
[Serializable, NetSerializable]
public sealed partial class ClearSlagDoAfterEvent : DoAfterEvent
{
[DataField("solution", required: true)]
public Solution Solution = default!;
[DataField("amount", required: true)]
public FixedPoint2 Amount;
private ClearSlagDoAfterEvent()
{
}
public ClearSlagDoAfterEvent(Solution solution, FixedPoint2 amount)
{
Solution = solution;
Amount = amount;
}
public override DoAfterEvent Clone() => this;
}
}
@@ -0,0 +1,22 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen.Components
{
[NetworkedComponent]
public abstract partial class SharedDeepFriedComponent : Component
{
/// <summary>
/// How deep-fried is this item?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("crispiness")]
public int Crispiness { get; set; }
}
[Serializable, NetSerializable]
public enum DeepFriedVisuals : byte
{
Fried,
}
}
@@ -0,0 +1,12 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen.Components
{
public abstract partial class SharedDeepFryerComponent : Component { }
[Serializable, NetSerializable]
public enum DeepFryerVisuals : byte
{
Bubbling,
}
}
@@ -0,0 +1,5 @@
namespace Content.Shared.Nyanotrasen.Kitchen;
public abstract class SharedDeepfryerSystem : EntitySystem
{
}
@@ -0,0 +1,67 @@
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization;
namespace Content.Shared.Nyanotrasen.Kitchen.UI
{
[Serializable, NetSerializable]
public sealed class DeepFryerBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly FixedPoint2 OilLevel;
public readonly FixedPoint2 OilPurity;
public readonly FixedPoint2 FryingOilThreshold;
public readonly NetEntity[] ContainedEntities;
public DeepFryerBoundUserInterfaceState(
FixedPoint2 oilLevel,
FixedPoint2 oilPurity,
FixedPoint2 fryingOilThreshold,
NetEntity[] containedEntities)
{
OilLevel = oilLevel;
OilPurity = oilPurity;
FryingOilThreshold = fryingOilThreshold;
ContainedEntities = containedEntities;
}
}
[Serializable, NetSerializable]
public sealed class DeepFryerRemoveItemMessage : BoundUserInterfaceMessage
{
public readonly NetEntity Item;
public DeepFryerRemoveItemMessage(NetEntity item)
{
Item = item;
}
}
[Serializable, NetSerializable]
public sealed class DeepFryerInsertItemMessage : BoundUserInterfaceMessage
{
public DeepFryerInsertItemMessage() { }
}
[Serializable, NetSerializable]
public sealed class DeepFryerScoopVatMessage : BoundUserInterfaceMessage
{
public DeepFryerScoopVatMessage() { }
}
[Serializable, NetSerializable]
public sealed class DeepFryerClearSlagMessage : BoundUserInterfaceMessage
{
public DeepFryerClearSlagMessage() { }
}
[Serializable, NetSerializable]
public sealed class DeepFryerRemoveAllItemsMessage : BoundUserInterfaceMessage
{
public DeepFryerRemoveAllItemsMessage() { }
}
[NetSerializable, Serializable]
public enum DeepFryerUiKey
{
Key
}
}
+3 -3
View File
@@ -26,7 +26,7 @@ public static class JobRequirements
{
var sys = entManager.System<SharedRoleSystem>();
var requirements = sys.GetRoleRequirements(job);
return TryRequirementsMet(requirements, playTimes, out reason, entManager, protoManager, profile, isWhitelisted: isWhitelisted); // DeltaV
return TryRequirementsMet(requirements, playTimes, out reason, entManager, protoManager, profile, isWhitelisted: isWhitelisted);
}
/// <summary>
@@ -43,7 +43,7 @@ public static class JobRequirements
IEntityManager entManager,
IPrototypeManager protoManager,
HumanoidCharacterProfile? profile,
bool isWhitelisted = false) // DeltaV
bool isWhitelisted = false)
{
reason = null;
if (requirements == null)
@@ -51,7 +51,7 @@ public static class JobRequirements
foreach (var requirement in requirements)
{
if (!requirement.Check(entManager, protoManager, profile, playTimes, out reason, isWhitelisted)) // DeltaV
if (!requirement.Check(entManager, protoManager, profile, playTimes, out reason, isWhitelisted))
return false;
}
+9 -21
View File
@@ -112,27 +112,6 @@ public sealed partial class DCCVars
public static readonly CVarDef<int> YearOffset =
CVarDef.Create("game.current_year_offset", 550, CVar.SERVERONLY);
/*
* Footprints
*/
/// <summary>
/// Maximum number of footprints allowed per tile.
/// Won't allow for new footprints to spawn on the tile once reached.
/// Set to 0 to disable per-tile limiting.
/// </summary>
public static readonly CVarDef<int> MaxFootPrintsPerTile =
CVarDef.Create("footprints.max_per_tile", 2, CVar.REPLICATED);
/// <summary>
/// Maximum total number of footprints allowed on a single grid.
/// When this limit is reached, the oldest footprint on the grid will be deleted.
/// Set to 0 to disable global limiting.
/// </summary>
public static readonly CVarDef<int> MaxFootPrintsPerGrid =
CVarDef.Create("footprints.max_per_grid", 1000, CVar.REPLICATED);
/*
* Feedback webhook
*/
@@ -217,6 +196,15 @@ public sealed partial class DCCVars
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color in which the highlights will be displayed.");
/* Laying down combat */
/// <summary>
/// Modifier to apply to all melee attacks when laying down.
/// Don't increase this above 1...
/// </summary>
public static readonly CVarDef<float> LayingDownMeleeMod =
CVarDef.Create("game.laying_down_melee_mod", 0.25f, CVar.REPLICATED);
/// <summary>
/// Maximum number of characters in objective summaries.
/// </summary>
@@ -1,191 +0,0 @@
using Content.Shared._DV.Kitchen.Systems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared._DV.Kitchen.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedDeepFryerSystem))]
public sealed partial class DeepFryerComponent : Component
{
/// <summary>
/// The <see cref="EntityWhitelist"/> of items that fit into the deep fryer
/// </summary>
[DataField]
public EntityWhitelist Whitelist = new();
/// <summary>
/// The <see cref="EntityWhitelist"/> of items that will never fit into the deep fryer, even if on the whitelist
/// </summary>
[DataField]
public EntityWhitelist Blacklist = new();
/// <summary>
/// The <see cref="Solution"/> of cooking oil
/// </summary>
[DataField]
public string Solution = "vat_oil";
/// <summary>
/// The allowed reagents that will not cause a reaction when inserted
/// </summary>
[DataField]
public List<ProtoId<ReagentPrototype>> FryingOils = new();
/// <summary>
/// The container that holds items being fried
/// </summary>
[DataField]
public string ContainerName = "fryer_basket";
/// <summary>
/// Maximum number of items that can be in the fryer at once
/// </summary>
[DataField]
public int MaxItems = 3;
/// <summary>
/// Minimum amount of oil required to fry items (in units)
/// </summary>
[DataField]
public FixedPoint2 MinimumOilVolume = 25;
/// <summary>
/// Tracks cooking progress for each item in the fryer
/// Key: EntityUid of the item being fried
/// Value: CookingItem struct containing recipe and time
/// </summary>
[DataField]
public Dictionary<EntityUid, CookingItem> CookingItems = new();
/// <summary>
/// The <see cref="SoundSpecifier"/> that will play once an item finishes cooking
/// </summary>
[DataField]
public SoundSpecifier FinishedCookingSound = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg");
/// <summary>
/// The <see cref="SoundSpecifier"/> that will play once an item finishes burning
/// </summary>
[DataField]
public SoundSpecifier FinishedBurningSound = new SoundPathSpecifier("/Audio/Effects/drop.ogg");
/// <summary>
/// Current oil quality as a value from 0.0 (0%) to 1.0 (100%)
/// </summary>
[DataField, AutoNetworkedField]
public float OilQuality = 1.0f;
/// <summary>
/// How much the oil quality degrades per recipe cooked (as a percentage, e.g., 0.05 = 5%)
/// </summary>
[DataField]
public float OilDegradationPerRecipe = 0.05f;
/// <summary>
/// Degradation multiplier when at minimum oil volume (higher = faster degradation with less oil)
/// </summary>
[DataField]
public float MinOilVolumeDegradationMultiplier = 4.0f;
/// <summary>
/// How much quality is restored per unit of oil added (e.g., 0.01 = 1% quality per unit)
/// </summary>
[DataField]
public float OilQualityRestorationPerUnit = 0.01f;
/// <summary>
/// Chance (0.0 to 1.0) that BurnedResult will spawn when using Foul quality oil
/// </summary>
[DataField]
public float FoulOilBurnChance = 0.3f;
/// <summary>
/// Maps oil quality levels to the flavors that should be added to cooked items
/// </summary>
[DataField]
public Dictionary<OilQuality, List<ProtoId<FlavorPrototype>>> OilQualityFlavors = new();
/// <summary>
/// Time tolerance for multi-ingredient recipes (ingredients must be inserted within this window)
/// </summary>
[DataField]
public TimeSpan CookingTolerance = TimeSpan.FromSeconds(5);
/// <summary>
/// The time it will take for ingredients to start burning if not a part of any recipe
/// </summary>
[DataField]
public TimeSpan BaseBurnTime = TimeSpan.FromSeconds(30);
/// <summary>
/// The default result for when something burns
/// </summary>
[DataField]
public EntProtoId BaseBurnedResult = "FoodBadRecipe";
/// <summary>
/// Chance (0.0 to 1.0) that a thrown item will miss the fryer and land nearby instead.
/// Professional chefs always have a 0% miss chance regardless of this value.
/// </summary>
/// <seealso cref="ProfessionalChefComponent"/>
[DataField]
public float MissChance = 0.25f;
}
/// <summary>
/// Tracks an item being cooked in the deep fryer
/// </summary>
[DataDefinition]
public partial record struct CookingItem
{
/// <summary>
/// The deep fryer recipe being used to cook this item
/// </summary>
[DataField]
public ProtoId<DeepFryerRecipePrototype>? Recipe;
/// <summary>
/// When this item started cooking or burning
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan TimeStarted;
/// <summary>
/// Whether this item has finished cooking and is now burning
/// </summary>
[DataField]
public bool IsBurning;
public CookingItem(ProtoId<DeepFryerRecipePrototype>? recipe, TimeSpan timeStarted, bool isBurning = false)
{
Recipe = recipe;
TimeStarted = timeStarted;
IsBurning = isBurning;
}
}
/// <summary>
/// Represents the quality level of oil in the deep fryer
/// </summary>
[Serializable, NetSerializable]
public enum OilQuality : byte
{
Pristine, // >= 90%
Clean, // >= 70%
Used, // >= 50%
Dirty, // >= 30%
Foul // >= 0%
}
[Serializable, NetSerializable]
public enum DeepFryerVisuals : byte
{
Bubbling
}
@@ -1,9 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Kitchen.Components;
/// <summary>
/// Players with this component will never miss a throw into the deep fryer's basket.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ProfessionalChefComponent : Component;
@@ -1,46 +0,0 @@
using Content.Shared.FixedPoint;
using Content.Shared.Kitchen;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Kitchen;
/// <summary>
/// The base recipe for the deep fryer.
/// </summary>
/// <remarks>
/// This only handles the deep-fryer related stuff like oil consumption and burning result,
/// while the <see cref="FoodRecipePrototype"/> handles the recipe and results themselves.
/// This is done because while FoodRecipe is not generic and hardcoded to microwaves,
/// we can still reuse it for guidebook generation without duping the code.
/// </remarks>
[Prototype]
public sealed class DeepFryerRecipePrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The <see cref="FoodRecipePrototype"/> to inherit from
/// </summary>
[DataField(required: true)]
public ProtoId<FoodRecipePrototype> BaseRecipe;
/// <summary>
/// How long will it take for this food to burn once it finishes cooking?
/// </summary>
[DataField]
public TimeSpan BurnTime = TimeSpan.FromSeconds(30);
/// <summary>
/// How many units of oil will this recipe use up?
/// </summary>
[DataField]
public FixedPoint2 OilConsumption = 2;
/// <summary>
/// The <see cref="EntityPrototype"/> this recipe will spawn when it finishes burning.
/// </summary>
[DataField]
public EntProtoId BurnedResult = "FoodBadRecipe";
}
@@ -1,320 +0,0 @@
using Content.Shared._DV.Kitchen.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Whitelist;
using Content.Shared.Verbs;
using Content.Shared.Hands.EntitySystems;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Kitchen.Systems;
public abstract class SharedDeepFryerSystem : EntitySystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] protected readonly SharedSolutionContainerSystem Solution = default!;
[Dependency] protected readonly SharedTransformSystem Xform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeepFryerComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DeepFryerComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<DeepFryerComponent, DeepFryerInsertDoAfterEvent>(OnInsertDoAfter);
SubscribeLocalEvent<DeepFryerComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerbs);
SubscribeLocalEvent<DeepFryerComponent, ExaminedEvent>(OnExamined);
}
private void OnComponentInit(Entity<DeepFryerComponent> ent, ref ComponentInit args)
{
_container.EnsureContainer<Container>(ent, ent.Comp.ContainerName);
}
private void OnExamined(Entity<DeepFryerComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution) && solution.Volume <= 0)
return;
var qualityLevel = GetOilQualityLevel(ent.Comp.OilQuality);
var (color, labelName) = GetOilQualityInfo(qualityLevel);
args.PushMarkup(Loc.GetString("deep-fryer-oil-quality-examine",
("color", color.ToHex()),
("state", labelName)));
}
private void OnInteractUsing(Entity<DeepFryerComponent> ent, ref InteractUsingEvent args)
{
if (args.Handled)
return;
// Check if the item can be inserted
if (!CanInsertItem(ent, args.Used, out var reason))
{
Popup.PopupClient(reason, ent, args.User);
return;
}
args.Handled = true;
// Start a do-after for inserting the item
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, 1f, new DeepFryerInsertDoAfterEvent(), ent, target: ent, used: args.Used)
{
BreakOnMove = true,
BreakOnDamage = true,
BlockDuplicate = true,
NeedHand = true
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
private void OnInsertDoAfter(Entity<DeepFryerComponent> ent, ref DeepFryerInsertDoAfterEvent args)
{
if (args.Cancelled || args.Handled || args.Used == null)
return;
// Re-check if we can still insert (things might have changed)
if (!CanInsertItem(ent, args.Used.Value, out var reason))
{
Popup.PopupClient(reason, ent, args.User);
return;
}
// Insert the item
if (TryInsertItem(ent, args.Used.Value, args.User))
{
args.Handled = true;
}
}
private void OnGetAlternativeVerbs(Entity<DeepFryerComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return;
var user = args.User;
// Create an eject verb for each item in the basket
foreach (var item in container.ContainedEntities)
{
var itemName = Name(item);
var itemUid = item;
var verb = new AlternativeVerb
{
Text = Loc.GetString("deep-fryer-eject-item", ("item", itemName)),
Category = VerbCategory.Eject,
Act = () => TryEjectItem(ent, itemUid, user),
Priority = 1
};
args.Verbs.Add(verb);
}
}
/// <summary>
/// Attempts to eject an item from the deep fryer
/// </summary>
[PublicAPI]
public bool TryEjectItem(Entity<DeepFryerComponent> ent, EntityUid item, EntityUid user)
{
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return false;
if (!container.Contains(item))
return false;
// Remove from container
if (!_container.Remove(item, container))
return false;
// Try to put in user's hands, otherwise drop at fryer location
if (!_hands.TryPickupAnyHand(user, item))
{
var xform = Transform(ent);
Xform.SetCoordinates(item, xform.Coordinates);
}
Popup.PopupClient(Loc.GetString("deep-fryer-eject-item-success", ("item", item)), ent, user);
return true;
}
/// <summary>
/// Checks if an item can be inserted into the deep fryer
/// </summary>
[PublicAPI]
public bool CanInsertItem(Entity<DeepFryerComponent> ent, EntityUid item, out string reason)
{
reason = string.Empty;
// Skip the popup entirely if we're transferring solutions
if (HasComp<SolutionTransferComponent>(item))
return false;
// Check blacklist first since it should override
if (_whitelist.IsBlacklistPass(ent.Comp.Blacklist, item))
{
reason = Loc.GetString("deep-fryer-blacklist-item");
return false;
}
// Check whitelist
if (!_whitelist.IsWhitelistPass(ent.Comp.Whitelist, item))
{
reason = Loc.GetString("deep-fryer-not-food");
return false;
}
// Get the container
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
{
reason = Loc.GetString("deep-fryer-no-container");
return false;
}
// Check if the fryer is full
if (container.ContainedEntities.Count >= ent.Comp.MaxItems)
{
reason = Loc.GetString("deep-fryer-full");
return false;
}
// Check if there's enough oil
if (!HasEnoughOil(ent))
{
reason = Loc.GetString("deep-fryer-insufficient-oil");
return false;
}
return true;
}
/// <summary>
/// Attempts to insert an item into the deep fryer
/// </summary>
[PublicAPI]
public bool TryInsertItem(Entity<DeepFryerComponent> ent, EntityUid item, EntityUid? user)
{
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return false;
// Insert the item
if (!_container.Insert(item, container))
return false;
Popup.PopupClient(Loc.GetString("deep-fryer-insert-item", ("item", item)), ent, user ?? EntityUid.Invalid);
return true;
}
/// <summary>
/// Checks if the fryer has enough oil to fry items
/// </summary>
protected bool HasEnoughOil(Entity<DeepFryerComponent> ent)
{
if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
return false;
// Check if there's enough total volume
if (solution.Volume < ent.Comp.MinimumOilVolume)
return false;
// Check if any of the reagents in the solution are valid frying oils
foreach (var reagent in solution.Contents)
{
if (ent.Comp.FryingOils.Contains(reagent.Reagent.Prototype))
return true;
}
return false;
}
/// <summary>
/// Gets the number of items currently in the fryer
/// </summary>
[PublicAPI]
public int GetItemCount(Entity<DeepFryerComponent> ent)
{
if (!_container.TryGetContainer(ent, ent.Comp.ContainerName, out var container))
return 0;
return container.ContainedEntities.Count;
}
/// <summary>
/// Determines the oil quality level from the quality value
/// </summary>
[PublicAPI]
public static OilQuality GetOilQualityLevel(float quality)
{
return quality switch
{
>= 0.9f => OilQuality.Pristine,
>= 0.7f => OilQuality.Clean,
>= 0.5f => OilQuality.Used,
>= 0.3f => OilQuality.Dirty,
_ => OilQuality.Foul
};
}
/// <summary>
/// Calculates the oil degradation multiplier based on current oil volume
/// Less oil = faster degradation
/// </summary>
protected float CalculateOilDegradationMultiplier(Entity<DeepFryerComponent> ent)
{
if (!Solution.TryGetSolution(ent.Owner, ent.Comp.Solution, out _, out var solution))
return 1.0f;
var maxVolume = solution.MaxVolume;
var currentVolume = solution.Volume;
var minVolume = ent.Comp.MinimumOilVolume;
// If we don't have enough oil range, just return 1.0
if (maxVolume <= minVolume)
return 1.0f;
// Linear interpolation between 1.0x (at max volume) and MinOilVolumeDegradationMultiplier (at min volume)
// 1.0 + (maxVolume - currentVolume) / (maxVolume - minVolume) * (multiplier - 1.0)
var volumeRatio = (float)((maxVolume - currentVolume) / (maxVolume - minVolume));
var multiplier = 1.0f + volumeRatio * (ent.Comp.MinOilVolumeDegradationMultiplier - 1.0f);
return Math.Max(1.0f, multiplier);
}
/// <summary>
/// Gets the color and label name for a given oil quality level
/// </summary>
[PublicAPI]
public (Color color, string labelName) GetOilQualityInfo(OilQuality quality)
{
return quality switch
{
OilQuality.Pristine => (Color.Green, Loc.GetString("deep-fryer-oil-quality-pristine")),
OilQuality.Clean => (Color.White, Loc.GetString("deep-fryer-oil-quality-clean")),
OilQuality.Used => (Color.Yellow, Loc.GetString("deep-fryer-oil-quality-used")),
OilQuality.Dirty => (Color.Orange, Loc.GetString("deep-fryer-oil-quality-dirty")),
OilQuality.Foul => (Color.Red, Loc.GetString("deep-fryer-oil-quality-foul")),
_ => (Color.White, Loc.GetString("deep-fryer-oil-quality-unknown"))
};
}
}
[Serializable, NetSerializable]
public sealed partial class DeepFryerInsertDoAfterEvent : SimpleDoAfterEvent;
+7 -22
View File
@@ -5,30 +5,19 @@ using Robust.Shared.GameStates;
namespace Content.Shared._DV.Mail;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedMailSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] // TODO: Access & PublicAPI for Commands
public sealed partial class MailComponent : Component
{
/// <summary>
/// The full name of the person who should receive this mail.
/// </summary>
[DataField, AutoNetworkedField]
public string Recipient = "Unknown";
public string Recipient = "None";
/// <summary>
/// The job title of the person who should receive this mail.
/// </summary>
[DataField, AutoNetworkedField]
public string RecipientJob = "Unknown";
public string RecipientJob = "None";
/// <summary>
/// Whether this mail is currently locked.
/// </summary>
/// <remarks>
/// Why do we not use LockComponent?
/// Because this can't be locked again,
/// and we have special conditions for unlocking,
/// and we don't want to add a verb.
/// </remarks>
// Why do we not use LockComponent?
// Because this can't be locked again,
// and we have special conditions for unlocking,
// and we don't want to add a verb.
[DataField, AutoNetworkedField]
public bool IsLocked = true;
@@ -119,9 +108,5 @@ public sealed partial class MailComponent : Component
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
/// <summary>
/// Cancellation token for the priority mail timer.
/// Used to cancel the penalty timer when priority mail is successfully delivered.
/// </summary>
public CancellationTokenSource? PriorityCancelToken;
}
@@ -10,7 +10,7 @@ namespace Content.Shared._DV.Mail;
/// This is for the mail teleporter.
/// Random mail will be teleported to this every few minutes.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause, Access(typeof(SharedMailSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] // TODO: Access & PublicAPI for Commands
public sealed partial class MailTeleporterComponent : Component
{
/// <summary>
+2 -111
View File
@@ -1,8 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared._DV.Cargo.Components;
using Content.Shared._DV.Cargo.Systems;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Cargo;
@@ -16,14 +14,12 @@ using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Objectives.Components;
using Content.Shared.PDA;
using Content.Shared.Popups;
using Content.Shared.Station;
using Content.Shared.Tag;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
@@ -43,8 +39,7 @@ public abstract class SharedMailSystem : EntitySystem
[Dependency] private readonly SharedCargoSystem _cargo = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedIdCardSystem _idCard = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] protected readonly SharedIdCardSystem IdCard = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] protected readonly SharedStationSystem Station = default!;
[Dependency] protected readonly TagSystem Tag = default!;
@@ -89,7 +84,7 @@ public abstract class SharedMailSystem : EntitySystem
if (HasComp<PdaComponent>(args.Used)) // Can we find it in a PDA if the user is using that?
{
_idCard.TryGetIdCard(args.Used, out var pdaId);
IdCard.TryGetIdCard(args.Used, out var pdaId);
idCard = pdaId;
}
@@ -387,96 +382,6 @@ public abstract class SharedMailSystem : EntitySystem
action(station, logisticStats);
}
}
/// <summary>
/// Try to match a mail receiver to a mail teleporter.
/// </summary>
[PublicAPI]
public bool TryGetMailTeleporterForReceiver(EntityUid receiverUid, [NotNullWhen(true)] out MailTeleporterComponent? teleporterComponent, [NotNullWhen(true)] out EntityUid? teleporterUid)
{
var query = EntityQueryEnumerator<MailTeleporterComponent>();
var receiverStation = Station.GetOwningStation(receiverUid);
while (query.MoveNext(out var uid, out var mailTeleporter))
{
var teleporterStation = Station.GetOwningStation(uid);
if (receiverStation != teleporterStation)
continue;
teleporterComponent = mailTeleporter;
teleporterUid = uid;
return true;
}
teleporterComponent = null;
teleporterUid = null;
return false;
}
/// <summary>
/// Try to construct a recipient struct for a mail parcel based on a receiver.
/// </summary>
[PublicAPI]
public bool TryGetMailRecipientForReceiver(EntityUid receiverUid, [NotNullWhen(true)] out MailRecipient? recipient)
{
if (_idCard.TryFindIdCard(receiverUid, out var idCard)
&& TryComp<AccessComponent>(idCard.Owner, out var access)
&& idCard.Comp.FullName != null)
{
var accessTags = access.Tags;
var mayReceivePriorityMail = !(_mind.GetMind(receiverUid) == null);
recipient = new MailRecipient(
idCard.Comp.FullName,
idCard.Comp.LocalizedJobTitle ?? idCard.Comp.JobTitle ?? "Unknown",
idCard.Comp.JobIcon,
accessTags,
mayReceivePriorityMail);
return true;
}
recipient = null;
return false;
}
/// <summary>
/// Sets whether the mail is fragile.
/// </summary>
[PublicAPI]
public void SetFragile(Entity<MailComponent?> ent, bool isFragile)
{
if (!Resolve(ent, ref ent.Comp) || ent.Comp.IsFragile == isFragile)
return;
ent.Comp.IsFragile = isFragile;
Dirty(ent);
}
/// <summary>
/// Sets whether the mail is priority mail.
/// </summary>
[PublicAPI]
public void SetPriority(Entity<MailComponent?> ent, bool isPriority)
{
if (!Resolve(ent, ref ent.Comp) || ent.Comp.IsPriority == isPriority)
return;
ent.Comp.IsPriority = isPriority;
Dirty(ent);
}
/// <summary>
/// Sets whether the mail is a large package.
/// </summary>
[PublicAPI]
public void SetLarge(Entity<MailComponent?> ent, bool isLarge)
{
if (!Resolve(ent, ref ent.Comp) || ent.Comp.IsLarge == isLarge)
return;
ent.Comp.IsLarge = isLarge;
Dirty(ent);
}
}
/// <summary>
@@ -514,17 +419,3 @@ public struct MailEntityStrings
public string DescClose;
public string DescFar;
}
public struct MailRecipient(
string name,
string job,
string jobIcon,
HashSet<ProtoId<AccessLevelPrototype>> accessTags,
bool mayReceivePriorityMail)
{
public readonly string Name = name;
public readonly string Job = job;
public readonly string JobIcon = jobIcon;
public readonly HashSet<ProtoId<AccessLevelPrototype>> AccessTags = accessTags;
public readonly bool MayReceivePriorityMail = mayReceivePriorityMail;
}
@@ -1,10 +0,0 @@
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
namespace Content.Shared._DV.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a Internal Affairs Agent.
/// </summary>
[RegisterComponent]
public sealed partial class NTAgentRoleComponent : BaseMindRoleComponent;
@@ -26,7 +26,7 @@ public sealed partial class WhitelistRequirement : JobRequirement
if (isWhitelisted)
return true;
reason = FormattedMessage.FromMarkupOrThrow(Loc.GetString("playtime-deny-reason-not-whitelisted"));
reason = FormattedMessage.FromMarkup(Loc.GetString("playtime-deny-reason-not-whitelisted"));
return false;
}
}
@@ -0,0 +1,46 @@
using Content.Shared._DV.CCVars;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Standing;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Standing;
/// <summary>
/// Prevents shooting and makes melee weaker while you are laying down (R)
/// </summary>
public sealed class LayingDownCombatSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
private DamageModifierSet _meleeMod = new();
public override void Initialize()
{
base.Initialize();
// subscribe to LayingDownComponent instead of StandingState so it only applies to mobs that can lie down on keypress
SubscribeLocalEvent<StandingStateComponent, GetMeleeDamageEvent>(OnGetMeleeDamage);
Subs.CVar(_cfg, DCCVars.LayingDownMeleeMod, mod =>
{
_meleeMod.Coefficients.Clear();
foreach (var proto in _proto.EnumeratePrototypes<DamageTypePrototype>())
{
_meleeMod.Coefficients.Add(proto.ID, mod);
}
}, true);
}
private void OnGetMeleeDamage(Entity<StandingStateComponent> ent, ref GetMeleeDamageEvent args)
{
if (!_standing.IsDown((ent, ent.Comp)))
return;
args.Modifiers.Add(_meleeMod);
}
}

Some files were not shown because too many files have changed in this diff Show More