mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-29 23:57:29 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8d8ea3f3 |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+2
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user