Merge branch 'master' of https://github.com/space-wizards/space-station-14 into FoodSizeAdjustment
This commit is contained in:
@@ -334,7 +334,12 @@ namespace Content.Client.Actions
|
||||
|
||||
private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
|
||||
{
|
||||
if (args.Handled || args.Input.EntityUid is not { Valid: true } entity)
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (args.Input.EntityUid is not { Valid: true } entity)
|
||||
return;
|
||||
|
||||
// let world target component handle it
|
||||
@@ -345,8 +350,6 @@ namespace Content.Client.Actions
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var action = args.Action;
|
||||
var user = args.User;
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<Control
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:viewport="clr-namespace:Content.Client.Viewport"
|
||||
MouseFilter="Stop">
|
||||
<PanelContainer StyleClasses="BackgroundDark" Name="AdminCameraWindowRoot" Access="Public">
|
||||
<BoxContainer Orientation="Vertical" Access="Public">
|
||||
<!-- Camera -->
|
||||
<Control VerticalExpand="True" Name="CameraViewBox">
|
||||
<viewport:ScalingViewport Name="CameraView"
|
||||
MinSize="100 100"
|
||||
MouseFilter="Ignore" />
|
||||
</Control>
|
||||
<!-- Controller buttons -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
|
||||
<Button StyleClasses="OpenRight" Name="FollowButton" HorizontalExpand="True" Access="Public" Text="{Loc 'admin-camera-window-follow'}" />
|
||||
<Button StyleClasses="OpenLeft" Name="PopControl" HorizontalExpand="True" Access="Public" Text="{Loc 'admin-camera-window-pop-out'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Eye;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Administration.UI.AdminCamera;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AdminCameraControl : Control
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
|
||||
public event Action? OnFollow;
|
||||
public event Action? OnPopoutControl;
|
||||
|
||||
private readonly EyeLerpingSystem _eyeLerpingSystem;
|
||||
private readonly FixedEye _defaultEye = new();
|
||||
private AdminCameraEuiState? _nextState;
|
||||
|
||||
private const float MinimumZoom = 0.1f;
|
||||
private const float MaximumZoom = 2.0f;
|
||||
|
||||
public EntityUid? CurrentCamera;
|
||||
public float Zoom = 1.0f;
|
||||
|
||||
public bool IsPoppedOut;
|
||||
|
||||
public AdminCameraControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_eyeLerpingSystem = _entManager.System<EyeLerpingSystem>();
|
||||
|
||||
CameraView.Eye = _defaultEye;
|
||||
|
||||
FollowButton.OnPressed += _ => OnFollow?.Invoke();
|
||||
PopControl.OnPressed += _ => OnPopoutControl?.Invoke();
|
||||
CameraView.OnResized += OnResized;
|
||||
}
|
||||
|
||||
private new void OnResized()
|
||||
{
|
||||
var width = Math.Max(CameraView.PixelWidth, (int)Math.Floor(CameraView.MinWidth));
|
||||
var height = Math.Max(CameraView.PixelHeight, (int)Math.Floor(CameraView.MinHeight));
|
||||
|
||||
CameraView.ViewportSize = new Vector2i(width, height);
|
||||
}
|
||||
|
||||
protected override void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
base.MouseWheel(args);
|
||||
|
||||
if (CameraView.Eye == null)
|
||||
return;
|
||||
|
||||
Zoom = Math.Clamp(Zoom - args.Delta.Y * 0.15f * Zoom, MinimumZoom, MaximumZoom);
|
||||
CameraView.Eye.Zoom = new Vector2(Zoom, Zoom);
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
public void SetState(AdminCameraEuiState state)
|
||||
{
|
||||
_nextState = state;
|
||||
}
|
||||
|
||||
// I know that this is awful, but I copied this from the solution editor anyways.
|
||||
// This is needed because EUIs update before the gamestate is applied, which means it will fail to get the uid from the net entity.
|
||||
// The suggestion from the comment in the solution editor saying to use a BUI is not ideal either:
|
||||
// - We would need to bind the UI to an entity, but with how BUIs currently work we cannot open it in the same tick as we spawn that entity on the server.
|
||||
// - We want the UI opened by the user session, not by their currently attached entity. Otherwise it would close in cases where admins move from one entity to another, for example when ghosting.
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
if (_nextState == null || _timing.LastRealTick < _nextState.Tick) // make sure the last gamestate has been applied
|
||||
return;
|
||||
|
||||
if (!_entManager.TryGetEntity(_nextState.Camera, out var cameraUid))
|
||||
return;
|
||||
|
||||
if (CurrentCamera == null)
|
||||
{
|
||||
_eyeLerpingSystem.AddEye(cameraUid.Value);
|
||||
CurrentCamera = cameraUid;
|
||||
}
|
||||
else if (CurrentCamera != cameraUid)
|
||||
{
|
||||
_eyeLerpingSystem.RemoveEye(CurrentCamera.Value);
|
||||
_eyeLerpingSystem.AddEye(cameraUid.Value);
|
||||
CurrentCamera = cameraUid;
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent<EyeComponent>(CurrentCamera, out var eye))
|
||||
CameraView.Eye = eye.Eye ?? _defaultEye;
|
||||
}
|
||||
}
|
||||
117
Content.Client/Administration/UI/AdminCamera/AdminCameraEui.cs
Normal file
117
Content.Client/Administration/UI/AdminCamera/AdminCameraEui.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Eui;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Eui;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Administration.UI.AdminCamera;
|
||||
|
||||
/// <summary>
|
||||
/// Admin Eui for opening a viewport window to observe entities.
|
||||
/// Use the "Open Camera" admin verb or the "camera" command to open.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed partial class AdminCameraEui : BaseEui
|
||||
{
|
||||
private readonly AdminCameraWindow _window;
|
||||
private readonly AdminCameraControl _control;
|
||||
|
||||
// If not null the camera is in "popped out" mode and is in an external window.
|
||||
private OSWindow? _OSWindow;
|
||||
|
||||
// The last location the window was located at in game.
|
||||
// Is used for getting knowing where to "pop in" external windows.
|
||||
private Vector2 _lastLocation;
|
||||
|
||||
public AdminCameraEui()
|
||||
{
|
||||
_window = new AdminCameraWindow();
|
||||
_control = new AdminCameraControl();
|
||||
|
||||
_window.Contents.AddChild(_control);
|
||||
|
||||
_control.OnFollow += () => SendMessage(new AdminCameraFollowMessage());
|
||||
_window.OnClose += () =>
|
||||
{
|
||||
if (!_control.IsPoppedOut)
|
||||
SendMessage(new CloseEuiMessage());
|
||||
};
|
||||
|
||||
_control.OnPopoutControl += () =>
|
||||
{
|
||||
if (_control.IsPoppedOut)
|
||||
PopIn();
|
||||
else
|
||||
PopOut();
|
||||
};
|
||||
}
|
||||
|
||||
// Pop the window out into an external OS window
|
||||
private void PopOut()
|
||||
{
|
||||
_lastLocation = _window.Position;
|
||||
|
||||
// TODO: When there is a way to have a minimum window size, enforce something!
|
||||
_OSWindow = new OSWindow
|
||||
{
|
||||
SetSize = _window.Size,
|
||||
Title = _window.Title ?? Loc.GetString("admin-camera-window-title-placeholder"),
|
||||
};
|
||||
|
||||
_OSWindow.Show();
|
||||
|
||||
if (_OSWindow.Root == null)
|
||||
return;
|
||||
|
||||
_control.Orphan();
|
||||
_OSWindow.Root.AddChild(_control);
|
||||
|
||||
_OSWindow.Closed += () =>
|
||||
{
|
||||
if (_control.IsPoppedOut)
|
||||
SendMessage(new CloseEuiMessage());
|
||||
};
|
||||
|
||||
_control.IsPoppedOut = true;
|
||||
_control.PopControl.Text = Loc.GetString("admin-camera-window-pop-in");
|
||||
|
||||
_window.Close();
|
||||
}
|
||||
|
||||
// Pop the window back into the in game window.
|
||||
private void PopIn()
|
||||
{
|
||||
_control.Orphan();
|
||||
_window.Contents.AddChild(_control);
|
||||
|
||||
_window.Open(_lastLocation);
|
||||
|
||||
_control.IsPoppedOut = false;
|
||||
_control.PopControl.Text = Loc.GetString("admin-camera-window-pop-out");
|
||||
|
||||
_OSWindow?.Close();
|
||||
_OSWindow = null;
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
base.Opened();
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
public override void Closed()
|
||||
{
|
||||
base.Closed();
|
||||
_window.Close();
|
||||
}
|
||||
|
||||
public override void HandleState(EuiStateBase baseState)
|
||||
{
|
||||
if (baseState is not AdminCameraEuiState state)
|
||||
return;
|
||||
|
||||
_window.SetState(state);
|
||||
_control.SetState(state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc admin-camera-window-title-placeholder}"
|
||||
SetSize="425 550"
|
||||
MinSize="200 225"
|
||||
Name="Window">
|
||||
</DefaultWindow>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Administration.UI.AdminCamera;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AdminCameraWindow : DefaultWindow
|
||||
{
|
||||
public AdminCameraWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
ContentsContainer.Margin = new Thickness(5, 0, 5, 0);
|
||||
}
|
||||
|
||||
public void SetState(AdminCameraEuiState state)
|
||||
{
|
||||
Title = Loc.GetString("admin-camera-window-title", ("name", state.Name));
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
<Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/>
|
||||
<Button Name="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/>
|
||||
<controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/>
|
||||
<Button Name="CameraButton" Text="{Loc player-panel-camera}" Disabled="True"/>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed partial class PlayerPanel : FancyWindow
|
||||
public event Action<NetUserId?>? OnOpenBans;
|
||||
public event Action<NetUserId?>? OnAhelp;
|
||||
public event Action<string?>? OnKick;
|
||||
public event Action<string?>? OnCamera;
|
||||
public event Action<NetUserId?>? OnOpenBanPanel;
|
||||
public event Action<NetUserId?, bool>? OnWhitelistToggle;
|
||||
public event Action? OnFollow;
|
||||
@@ -33,26 +34,27 @@ public sealed partial class PlayerPanel : FancyWindow
|
||||
|
||||
public PlayerPanel(IClientAdminManager adminManager)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_adminManager = adminManager;
|
||||
RobustXamlLoader.Load(this);
|
||||
_adminManager = adminManager;
|
||||
|
||||
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? "");
|
||||
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
|
||||
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
|
||||
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
|
||||
ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
|
||||
AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
|
||||
WhitelistToggle.OnPressed += _ =>
|
||||
{
|
||||
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
|
||||
SetWhitelisted(!_isWhitelisted);
|
||||
};
|
||||
FollowButton.OnPressed += _ => OnFollow?.Invoke();
|
||||
FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
|
||||
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
|
||||
LogsButton.OnPressed += _ => OnLogs?.Invoke();
|
||||
DeleteButton.OnPressed += _ => OnDelete?.Invoke();
|
||||
RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
|
||||
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? "");
|
||||
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
|
||||
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
|
||||
CameraButton.OnPressed += _ => OnCamera?.Invoke(TargetUsername);
|
||||
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
|
||||
ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
|
||||
AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
|
||||
WhitelistToggle.OnPressed += _ =>
|
||||
{
|
||||
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
|
||||
SetWhitelisted(!_isWhitelisted);
|
||||
};
|
||||
FollowButton.OnPressed += _ => OnFollow?.Invoke();
|
||||
FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
|
||||
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
|
||||
LogsButton.OnPressed += _ => OnLogs?.Invoke();
|
||||
DeleteButton.OnPressed += _ => OnDelete?.Invoke();
|
||||
RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
|
||||
}
|
||||
|
||||
public void SetUsername(string player)
|
||||
@@ -122,6 +124,7 @@ public sealed partial class PlayerPanel : FancyWindow
|
||||
{
|
||||
BanButton.Disabled = !_adminManager.CanCommand("banpanel");
|
||||
KickButton.Disabled = !_adminManager.CanCommand("kick");
|
||||
CameraButton.Disabled = !_adminManager.CanCommand("camera");
|
||||
NotesButton.Disabled = !_adminManager.CanCommand("adminnotes");
|
||||
ShowBansButton.Disabled = !_adminManager.CanCommand("banlist");
|
||||
WhitelistToggle.Disabled =
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class PlayerPanelEui : BaseEui
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IClipboardManager _clipboard = default!;
|
||||
|
||||
private PlayerPanel PlayerPanel { get; }
|
||||
private PlayerPanel PlayerPanel { get; }
|
||||
|
||||
public PlayerPanelEui()
|
||||
{
|
||||
@@ -25,6 +25,7 @@ public sealed class PlayerPanelEui : BaseEui
|
||||
PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\"");
|
||||
// Kick command does not support GUIDs
|
||||
PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\"");
|
||||
PlayerPanel.OnCamera += username => _console.ExecuteCommand($"camera \"{username}\"");
|
||||
PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\"");
|
||||
PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\"");
|
||||
PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\"");
|
||||
@@ -37,7 +38,7 @@ public sealed class PlayerPanelEui : BaseEui
|
||||
PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage());
|
||||
PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
|
||||
PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
|
||||
PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
|
||||
PlayerPanel.OnDelete += () => SendMessage(new PlayerPanelDeleteMessage());
|
||||
PlayerPanel.OnFollow += () => SendMessage(new PlayerPanelFollowMessage());
|
||||
|
||||
PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());
|
||||
|
||||
@@ -62,11 +62,11 @@ public sealed class AnomalySystem : SharedAnomalySystem
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<AnomalySupercriticalComponent, SpriteComponent>();
|
||||
var query = EntityQueryEnumerator<AnomalyComponent, AnomalySupercriticalComponent, SpriteComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var super, out var sprite))
|
||||
while (query.MoveNext(out var uid, out var anomaly, out var super, out var sprite))
|
||||
{
|
||||
var completion = 1f - (float)((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
|
||||
var completion = 1f - (float) ((super.EndTime - _timing.CurTime) / anomaly.SupercriticalDuration);
|
||||
var scale = completion * (super.MaxScaleAmount - 1f) + 1f;
|
||||
_sprite.SetScale((uid, sprite), new Vector2(scale, scale));
|
||||
|
||||
|
||||
@@ -162,10 +162,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
{
|
||||
var list = new List<AtmosMonitoringConsoleLine>();
|
||||
|
||||
foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData)
|
||||
foreach (var ((netId, layer, pipeColor), atmosPipeData) in chunk.AtmosPipeData)
|
||||
{
|
||||
// Determine the correct coloration for the pipe
|
||||
var color = Color.FromHex(hexColor) * _basePipeNetColor;
|
||||
var color = pipeColor * _basePipeNetColor;
|
||||
|
||||
if (FocusNetId != null && FocusNetId != netId)
|
||||
color *= _unfocusedPipeNetColor;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Used to change the appearance of gas canisters.
|
||||
/// </summary>
|
||||
public sealed class GasCanisterAppearanceSystem : VisualizerSystem<GasCanisterComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (!AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old)
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.HasIndex(protoName))
|
||||
return;
|
||||
|
||||
// Create the given prototype and get its first layer.
|
||||
var tempUid = Spawn(protoName);
|
||||
SpriteSystem.LayerSetRsiState(uid, 0, SpriteSystem.LayerGetRsiState(tempUid, 0));
|
||||
QueueDel(tempUid);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,46 @@
|
||||
using Content.Client.Atmos.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Content.Client.UserInterface.Systems.Storage.Controls;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Item;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PipeColorVisualsComponent, GetInhandVisualsEvent>(OnGetVisuals);
|
||||
SubscribeLocalEvent<PipeColorVisualsComponent, BeforeRenderInGridEvent>(OnDrawInGrid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is used to display the color changes of the pipe on the screen..
|
||||
/// </summary>
|
||||
private void OnGetVisuals(Entity<PipeColorVisualsComponent> item, ref GetInhandVisualsEvent args)
|
||||
{
|
||||
foreach (var (_, layerData) in args.Layers)
|
||||
{
|
||||
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
|
||||
layerData.Color = pipeColor.Color;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is used to change the pipe's color in a container grid.
|
||||
/// </summary>
|
||||
private void OnDrawInGrid(Entity<PipeColorVisualsComponent> item, ref BeforeRenderInGridEvent args)
|
||||
{
|
||||
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
|
||||
args.Color = pipeColor.Color;
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, PipeColorVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite)
|
||||
@@ -15,6 +50,8 @@ public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisual
|
||||
var layer = sprite[PipeVisualLayers.Pipe];
|
||||
layer.Color = color.WithAlpha(layer.Color.A);
|
||||
}
|
||||
|
||||
_itemSystem.VisualsChanged(uid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2007/xaml"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
MinSize="500 500" Resizable="True" Title="Air Alarm">
|
||||
MinSize="500 500" Resizable="True" Title="{Loc air-alarm-ui-title}">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
|
||||
<!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.Graphics;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -36,7 +37,7 @@ namespace Content.Client.Cooldown
|
||||
if (Progress >= 0f)
|
||||
{
|
||||
var hue = (5f / 18f) * lerp;
|
||||
color = Color.FromHsv((hue, 0.75f, 0.75f, 0.50f));
|
||||
color = Color.FromHsv(new Vector4(hue, 0.75f, 0.75f, 0.50f));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,7 +1,126 @@
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Client.Stunnable;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Damage.Systems;
|
||||
|
||||
public sealed partial class StaminaSystem : SharedStaminaSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly StunSystem _stun = default!; // Clientside Stun System
|
||||
|
||||
private const string StaminaAnimationKey = "stamina";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StaminaComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeLocalEvent<ActiveStaminaComponent, ComponentShutdown>(OnActiveStaminaShutdown);
|
||||
SubscribeLocalEvent<StaminaComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
}
|
||||
|
||||
protected override void OnStamHandleState(Entity<StaminaComponent> entity, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
base.OnStamHandleState(entity, ref args);
|
||||
|
||||
TryStartAnimation(entity);
|
||||
}
|
||||
|
||||
private void OnActiveStaminaShutdown(Entity<ActiveStaminaComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
// If we don't have active stamina, we shouldn't have stamina damage. If the update loop can trust it we can trust it.
|
||||
if (!TryComp<StaminaComponent>(entity, out var stamina))
|
||||
return;
|
||||
|
||||
StopAnimation((entity, stamina));
|
||||
}
|
||||
|
||||
protected override void OnShutdown(Entity<StaminaComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
base.OnShutdown(entity, ref args);
|
||||
|
||||
StopAnimation(entity);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(Entity<StaminaComponent> ent, ref MobStateChangedEvent args)
|
||||
{
|
||||
if (args.NewMobState == MobState.Dead)
|
||||
StopAnimation(ent);
|
||||
}
|
||||
|
||||
private void TryStartAnimation(Entity<StaminaComponent> entity)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
// If the animation is running, the system should update it accordingly
|
||||
// If we're below the threshold to animate, don't try to animate
|
||||
// If we're in stamcrit don't override it
|
||||
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage || _animation.HasRunningAnimation(entity, StaminaAnimationKey))
|
||||
return;
|
||||
|
||||
// Don't animate if we're dead
|
||||
if (_mobState.IsDead(entity))
|
||||
return;
|
||||
|
||||
entity.Comp.StartOffset = sprite.Offset;
|
||||
|
||||
PlayAnimation((entity, entity.Comp, sprite));
|
||||
}
|
||||
|
||||
private void StopAnimation(Entity<StaminaComponent, SpriteComponent?> entity)
|
||||
{
|
||||
if(!Resolve(entity, ref entity.Comp2))
|
||||
return;
|
||||
|
||||
_animation.Stop(entity.Owner, StaminaAnimationKey);
|
||||
entity.Comp1.StartOffset = entity.Comp2.Offset;
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(Entity<StaminaComponent> entity, ref AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != StaminaAnimationKey || !args.Finished || !TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
// stop looping if we're below the threshold
|
||||
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage)
|
||||
{
|
||||
_animation.Stop(entity.Owner, StaminaAnimationKey);
|
||||
_sprite.SetOffset((entity, sprite), entity.Comp.StartOffset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasComp<AnimationPlayerComponent>(entity))
|
||||
return;
|
||||
|
||||
PlayAnimation((entity, entity.Comp, sprite));
|
||||
}
|
||||
|
||||
private void PlayAnimation(Entity<StaminaComponent, SpriteComponent> entity)
|
||||
{
|
||||
var step = Math.Clamp((entity.Comp1.StaminaDamage - entity.Comp1.AnimationThreshold) /
|
||||
(entity.Comp1.CritThreshold - entity.Comp1.AnimationThreshold),
|
||||
0f,
|
||||
1f); // The things I do for project 0 warnings
|
||||
var frequency = entity.Comp1.FrequencyMin + step * entity.Comp1.FrequencyMod;
|
||||
var jitter = entity.Comp1.JitterAmplitudeMin + step * entity.Comp1.JitterAmplitudeMod;
|
||||
var breathing = entity.Comp1.BreathingAmplitudeMin + step * entity.Comp1.BreathingAmplitudeMod;
|
||||
|
||||
_animation.Play(entity.Owner,
|
||||
_stun.GetFatigueAnimation(entity.Comp2,
|
||||
frequency,
|
||||
entity.Comp1.Jitters,
|
||||
jitter * entity.Comp1.JitterMin,
|
||||
jitter * entity.Comp1.JitterMax,
|
||||
breathing,
|
||||
entity.Comp1.StartOffset,
|
||||
ref entity.Comp1.LastJitter),
|
||||
StaminaAnimationKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using Content.Shared.Devour;
|
||||
|
||||
namespace Content.Client.Devour;
|
||||
public sealed class DevourSystem : SharedDevourSystem
|
||||
{
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Disposal;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Disposal;
|
||||
using Content.Shared.Disposal.Unit;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Doors;
|
||||
|
||||
public sealed class DoorSystem : SharedDoorSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -85,8 +86,8 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
|
||||
state = DoorState.Closed;
|
||||
|
||||
if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
|
||||
UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi);
|
||||
if (AppearanceSystem.TryGetData<string>(entity, PaintableVisuals.Prototype, out var prototype, args.Component))
|
||||
UpdateSpriteLayers((entity.Owner, args.Sprite), prototype);
|
||||
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
|
||||
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
|
||||
@@ -139,14 +140,14 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string baseRsi)
|
||||
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string targetProto)
|
||||
{
|
||||
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
|
||||
{
|
||||
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
|
||||
if (!_prototypeManager.TryIndex(targetProto, out var target))
|
||||
return;
|
||||
}
|
||||
|
||||
_sprite.SetBaseRsi(sprite.AsNullable(), res.RSI);
|
||||
if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))
|
||||
return;
|
||||
|
||||
_sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class DrowsinessOverlay : Overlay
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
private readonly SharedStatusEffectsSystem _statusEffects = default!;
|
||||
private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
@@ -33,7 +33,7 @@ public sealed class DrowsinessOverlay : Overlay
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
|
||||
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
|
||||
|
||||
_drowsinessShader = _prototypeManager.Index(Shader).InstanceUnique();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
private DrowsinessOverlay _overlay = default!;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class RainbowOverlay : Overlay
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
private readonly SharedStatusEffectsSystem _statusEffects = default!;
|
||||
private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
@@ -41,7 +41,7 @@ public sealed class RainbowOverlay : Overlay
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
|
||||
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
|
||||
|
||||
_rainbowShader = _prototypeManager.Index(Shader).InstanceUnique();
|
||||
_config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.Holopad;
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace Content.Client.Input
|
||||
human.AddFunction(EngineKeyFunctions.MoveLeft);
|
||||
human.AddFunction(EngineKeyFunctions.MoveRight);
|
||||
human.AddFunction(EngineKeyFunctions.Walk);
|
||||
human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
|
||||
human.AddFunction(ContentKeyFunctions.SwapHands);
|
||||
human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
|
||||
human.AddFunction(ContentKeyFunctions.Drop);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Items.Systems;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Hands;
|
||||
|
||||
@@ -57,7 +57,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
|
||||
rotation += 2 * Math.PI;
|
||||
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
|
||||
{
|
||||
Rotation = rotation
|
||||
Rotation = rotation,
|
||||
User = GetNetEntity(player)
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -77,7 +78,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
|
||||
|
||||
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
|
||||
{
|
||||
Rotation = angle
|
||||
Rotation = angle,
|
||||
User = GetNetEntity(player)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
AddButton(EngineKeyFunctions.Walk);
|
||||
AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
|
||||
InitToggleWalk();
|
||||
AddButton(ContentKeyFunctions.ToggleKnockdown);
|
||||
|
||||
AddHeader("ui-options-header-camera");
|
||||
AddButton(EngineKeyFunctions.CameraRotateLeft);
|
||||
|
||||
@@ -195,7 +195,6 @@ public sealed class GeneratedParallaxCache : IPostInjectInit
|
||||
public required ResPath ConfigPath;
|
||||
public required Task<Texture> LoadTask;
|
||||
public required CancellationTokenSource CancellationSource;
|
||||
public ValueList<CancellationTokenRegistration> CancelRegistrations;
|
||||
|
||||
public int RefCount;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.APC;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
@@ -9,7 +9,7 @@ public static class StaticPowerSystem
|
||||
public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null)
|
||||
{
|
||||
if (receiver == null && !entManager.TryGetComponent(uid, out receiver))
|
||||
return false;
|
||||
return true;
|
||||
|
||||
return receiver.Powered;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
|
||||
namespace Content.Client.Power;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ using Robust.Shared.Physics;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
|
||||
@@ -220,9 +220,9 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
|
||||
|
||||
var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
|
||||
|
||||
var distance = gridCentre.Length();
|
||||
var gridDistance = (gridBody.LocalCenter - xform.LocalPosition).Length();
|
||||
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
|
||||
("distance", $"{distance:0.0}"));
|
||||
("distance", $"{gridDistance:0.0}"));
|
||||
|
||||
var mapCoords = _transform.GetWorldPosition(gUid);
|
||||
var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
|
||||
|
||||
43
Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs
Normal file
43
Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.SmartFridge;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private SmartFridgeMenu? _menu;
|
||||
|
||||
public SmartFridgeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<SmartFridgeMenu>();
|
||||
_menu.OnItemSelected += OnItemSelected;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (_menu is not {} menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
|
||||
return;
|
||||
|
||||
menu.SetFlavorText(Loc.GetString(fridge.FlavorText));
|
||||
menu.Populate((Owner, fridge));
|
||||
}
|
||||
|
||||
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
if (data is not SmartFridgeListData entry)
|
||||
return;
|
||||
SendPredictedMessage(new SmartFridgeDispenseItemMessage(entry.Entry));
|
||||
}
|
||||
}
|
||||
16
Content.Client/SmartFridge/SmartFridgeItem.xaml
Normal file
16
Content.Client/SmartFridge/SmartFridgeItem.xaml
Normal file
@@ -0,0 +1,16 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="4">
|
||||
<SpriteView
|
||||
Name="EntityView"
|
||||
Margin="4 0 0 0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
MinSize="32 32"
|
||||
/>
|
||||
<Label Name="NameLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
</BoxContainer>
|
||||
18
Content.Client/SmartFridge/SmartFridgeItem.xaml.cs
Normal file
18
Content.Client/SmartFridge/SmartFridgeItem.xaml.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SmartFridgeItem : BoxContainer
|
||||
{
|
||||
public SmartFridgeItem(EntityUid uid, string text)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
EntityView.SetEntity(uid);
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
}
|
||||
24
Content.Client/SmartFridge/SmartFridgeMenu.xaml
Normal file
24
Content.Client/SmartFridge/SmartFridgeMenu.xaml
Normal file
@@ -0,0 +1,24 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinHeight="450"
|
||||
MinWidth="350"
|
||||
Title="{Loc 'smart-fridge-component-title'}">
|
||||
<BoxContainer Name="MainContainer" Orientation="Vertical">
|
||||
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'smart-fridge-component-search-filter'}" HorizontalExpand="True" Margin ="4 4"/>
|
||||
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Name="LeftFlavorLabel" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'vending-machine-flavor-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
81
Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs
Normal file
81
Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.SmartFridge;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public record SmartFridgeListData(EntityUid Representative, SmartFridgeEntry Entry, int Amount) : ListData;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SmartFridgeMenu : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
|
||||
|
||||
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
|
||||
|
||||
public SmartFridgeMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
VendingContents.SearchBar = SearchBar;
|
||||
VendingContents.DataFilterCondition += DataFilterCondition;
|
||||
VendingContents.GenerateItem += GenerateButton;
|
||||
VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
|
||||
}
|
||||
|
||||
private bool DataFilterCondition(string filter, ListData data)
|
||||
{
|
||||
if (data is not SmartFridgeListData entry)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(filter))
|
||||
return true;
|
||||
|
||||
return entry.Entry.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private void GenerateButton(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not SmartFridgeListData entry)
|
||||
return;
|
||||
|
||||
var label = Loc.GetString("smart-fridge-list-item", ("item", entry.Entry.Name), ("amount", entry.Amount));
|
||||
button.AddChild(new SmartFridgeItem(entry.Representative, label));
|
||||
|
||||
button.ToolTip = label;
|
||||
button.StyleBoxOverride = _styleBox;
|
||||
}
|
||||
|
||||
public void Populate(Entity<SmartFridgeComponent> ent)
|
||||
{
|
||||
var listData = new List<ListData>();
|
||||
|
||||
foreach (var item in ent.Comp.Entries)
|
||||
{
|
||||
if (!ent.Comp.ContainedEntries.TryGetValue(item, out var items) || items.Count == 0)
|
||||
{
|
||||
listData.Add(new SmartFridgeListData(EntityUid.Invalid, item, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
var representative = _entityManager.GetEntity(items.First());
|
||||
listData.Add(new SmartFridgeListData(representative, item, items.Count));
|
||||
}
|
||||
}
|
||||
|
||||
VendingContents.PopulateList(listData);
|
||||
}
|
||||
|
||||
public void SetFlavorText(string flavor)
|
||||
{
|
||||
LeftFlavorLabel.Text = flavor;
|
||||
}
|
||||
}
|
||||
24
Content.Client/SmartFridge/SmartFridgeUISystem.cs
Normal file
24
Content.Client/SmartFridge/SmartFridgeUISystem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.SmartFridge;
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeUISystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SmartFridgeComponent, AfterAutoHandleStateEvent>(OnSmartFridgeAfterState);
|
||||
}
|
||||
|
||||
private void OnSmartFridgeAfterState(Entity<SmartFridgeComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
|
||||
return;
|
||||
|
||||
bui.Refresh();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,129 @@
|
||||
using Content.Shared.SprayPainter;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Graphics;
|
||||
using Content.Client.Items;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.SprayPainter;
|
||||
using Content.Shared.SprayPainter.Components;
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.SprayPainter;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state.
|
||||
/// </summary>
|
||||
public sealed class SprayPainterSystem : SharedSprayPainterSystem
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public List<SprayPainterEntry> Entries { get; private set; } = new();
|
||||
public List<SprayPainterDecalEntry> Decals = [];
|
||||
public Dictionary<string, List<string>> PaintableGroupsByCategory = new();
|
||||
public Dictionary<string, Dictionary<string, EntProtoId>> PaintableStylesByGroup = new();
|
||||
|
||||
protected override void CacheStyles()
|
||||
public override void Initialize()
|
||||
{
|
||||
base.CacheStyles();
|
||||
base.Initialize();
|
||||
|
||||
Entries.Clear();
|
||||
foreach (var style in Styles)
|
||||
Subs.ItemStatus<SprayPainterComponent>(ent => new StatusControl(ent));
|
||||
SubscribeLocalEvent<SprayPainterComponent, AfterAutoHandleStateEvent>(OnStateUpdate);
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
|
||||
CachePrototypes();
|
||||
}
|
||||
|
||||
private void OnStateUpdate(Entity<SprayPainterComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
protected override void UpdateUi(Entity<SprayPainterComponent> ent)
|
||||
{
|
||||
if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui))
|
||||
bui.Update();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
if (!args.WasModified<PaintableGroupCategoryPrototype>() || !args.WasModified<PaintableGroupPrototype>() || !args.WasModified<DecalPrototype>())
|
||||
return;
|
||||
|
||||
CachePrototypes();
|
||||
}
|
||||
|
||||
private void CachePrototypes()
|
||||
{
|
||||
PaintableGroupsByCategory.Clear();
|
||||
PaintableStylesByGroup.Clear();
|
||||
foreach (var category in Proto.EnumeratePrototypes<PaintableGroupCategoryPrototype>().OrderBy(x => x.ID))
|
||||
{
|
||||
var name = style.Name;
|
||||
string? iconPath = Groups
|
||||
.FindAll(x => x.StylePaths.ContainsKey(name))?
|
||||
.MaxBy(x => x.IconPriority)?.StylePaths[name];
|
||||
if (iconPath == null)
|
||||
var groupList = new List<string>();
|
||||
foreach (var groupId in category.Groups)
|
||||
{
|
||||
Entries.Add(new SprayPainterEntry(name, null));
|
||||
continue;
|
||||
if (!Proto.TryIndex(groupId, out var group))
|
||||
continue;
|
||||
|
||||
groupList.Add(groupId);
|
||||
PaintableStylesByGroup[groupId] = group.Styles;
|
||||
}
|
||||
|
||||
RSIResource doorRsi = _resourceCache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath));
|
||||
if (!doorRsi.RSI.TryGetState("closed", out var icon))
|
||||
{
|
||||
Entries.Add(new SprayPainterEntry(name, null));
|
||||
continue;
|
||||
}
|
||||
if (groupList.Count > 0)
|
||||
PaintableGroupsByCategory[category.ID] = groupList;
|
||||
}
|
||||
|
||||
Entries.Add(new SprayPainterEntry(name, icon.Frame0));
|
||||
Decals.Clear();
|
||||
foreach (var decalPrototype in Proto.EnumeratePrototypes<DecalPrototype>().OrderBy(x => x.ID))
|
||||
{
|
||||
if (!decalPrototype.Tags.Contains("station")
|
||||
&& !decalPrototype.Tags.Contains("markings")
|
||||
|| decalPrototype.Tags.Contains("dirty"))
|
||||
continue;
|
||||
|
||||
Decals.Add(new SprayPainterDecalEntry(decalPrototype.ID, decalPrototype.Sprite));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StatusControl : Control
|
||||
{
|
||||
private readonly RichTextLabel _label;
|
||||
private readonly Entity<SprayPainterComponent> _entity;
|
||||
private DecalPaintMode? _lastPaintingDecals = null;
|
||||
|
||||
public StatusControl(Entity<SprayPainterComponent> ent)
|
||||
{
|
||||
_entity = ent;
|
||||
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (_entity.Comp.DecalMode == _lastPaintingDecals)
|
||||
return;
|
||||
|
||||
_lastPaintingDecals = _entity.Comp.DecalMode;
|
||||
|
||||
string modeLocString = _entity.Comp.DecalMode switch
|
||||
{
|
||||
DecalPaintMode.Add => "spray-painter-item-status-add",
|
||||
DecalPaintMode.Remove => "spray-painter-item-status-remove",
|
||||
_ => "spray-painter-item-status-off"
|
||||
};
|
||||
|
||||
_label.SetMarkupPermissive(Robust.Shared.Localization.Loc.GetString("spray-painter-item-status-label",
|
||||
("mode", Robust.Shared.Localization.Loc.GetString(modeLocString))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SprayPainterEntry
|
||||
{
|
||||
public string Name;
|
||||
public Texture? Icon;
|
||||
|
||||
public SprayPainterEntry(string name, Texture? icon)
|
||||
{
|
||||
Name = name;
|
||||
Icon = icon;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A spray paintable decal, mapped by ID.
|
||||
/// </summary>
|
||||
public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite);
|
||||
|
||||
@@ -1,42 +1,96 @@
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.SprayPainter;
|
||||
using Content.Shared.SprayPainter.Components;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
public sealed class SprayPainterBoundUserInterface : BoundUserInterface
|
||||
/// <summary>
|
||||
/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category.
|
||||
/// </summary>
|
||||
public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
[ViewVariables]
|
||||
private SprayPainterWindow? _window;
|
||||
|
||||
public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<SprayPainterWindow>();
|
||||
|
||||
_window.OnSpritePicked = OnSpritePicked;
|
||||
_window.OnColorPicked = OnColorPicked;
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp))
|
||||
if (_window == null)
|
||||
{
|
||||
_window.Populate(EntMan.System<SprayPainterSystem>().Entries, comp.Index, comp.PickedColor, comp.ColorPalette);
|
||||
_window = this.CreateWindow<SprayPainterWindow>();
|
||||
|
||||
_window.OnSpritePicked += OnSpritePicked;
|
||||
_window.OnSetPipeColor += OnSetPipeColor;
|
||||
_window.OnTabChanged += OnTabChanged;
|
||||
_window.OnDecalChanged += OnDecalChanged;
|
||||
_window.OnDecalColorChanged += OnDecalColorChanged;
|
||||
_window.OnDecalAngleChanged += OnDecalAngleChanged;
|
||||
_window.OnDecalSnapChanged += OnDecalSnapChanged;
|
||||
}
|
||||
|
||||
var sprayPainter = EntMan.System<SprayPainterSystem>();
|
||||
_window.PopulateCategories(sprayPainter.PaintableStylesByGroup, sprayPainter.PaintableGroupsByCategory, sprayPainter.Decals);
|
||||
Update();
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainterComp))
|
||||
_window.SetSelectedTab(sprayPainterComp.SelectedTab);
|
||||
}
|
||||
|
||||
private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args)
|
||||
public override void Update()
|
||||
{
|
||||
SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (!EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainter))
|
||||
return;
|
||||
|
||||
_window.PopulateColors(sprayPainter.ColorPalette);
|
||||
if (sprayPainter.PickedColor != null)
|
||||
_window.SelectColor(sprayPainter.PickedColor);
|
||||
_window.SetSelectedStyles(sprayPainter.StylesByGroup);
|
||||
_window.SetSelectedDecal(sprayPainter.SelectedDecal);
|
||||
_window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
|
||||
_window.SetDecalColor(sprayPainter.SelectedDecalColor);
|
||||
_window.SetDecalSnap(sprayPainter.SnapDecals);
|
||||
}
|
||||
|
||||
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
|
||||
private void OnDecalSnapChanged(bool snap)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalSnapMessage(snap));
|
||||
}
|
||||
|
||||
private void OnDecalAngleChanged(int angle)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle));
|
||||
}
|
||||
|
||||
private void OnDecalColorChanged(Color? color)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalColorMessage(color));
|
||||
}
|
||||
|
||||
private void OnDecalChanged(ProtoId<DecalPrototype> protoId)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalMessage(protoId));
|
||||
}
|
||||
|
||||
private void OnTabChanged(int index, bool isSelectedTabWithDecals)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterTabChangedMessage(index, isSelectedTabWithDecals));
|
||||
}
|
||||
|
||||
private void OnSpritePicked(string group, string style)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style));
|
||||
}
|
||||
|
||||
private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
var key = _window?.IndexToColorKey(args.ItemIndex);
|
||||
SendMessage(new SprayPainterColorPickedMessage(key));
|
||||
SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
|
||||
}
|
||||
}
|
||||
|
||||
26
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
Normal file
26
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<controls:SprayPainterDecals
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'spray-painter-selected-decals'}" />
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<GridContainer Columns="7" Name="DecalsGrid">
|
||||
<!-- populated by code -->
|
||||
</GridContainer>
|
||||
</ScrollContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True" />
|
||||
<CheckBox Name="UseCustomColorCheckBox" Text="{Loc 'spray-painter-use-custom-color'}" />
|
||||
<CheckBox Name="SnapToTileCheckBox" Text="{Loc 'spray-painter-use-snap-to-tile'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'spray-painter-angle-rotation'}" />
|
||||
<SpinBox Name="AngleSpinBox" HorizontalExpand="True" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-90-sub'}" Name="SubAngleButton" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-reset'}" Name="SetZeroAngleButton" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-90-add'}" Name="AddAngleButton" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:SprayPainterDecals>
|
||||
174
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
Normal file
174
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Used to control decal painting parameters for the spray painter.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SprayPainterDecals : Control
|
||||
{
|
||||
public Action<ProtoId<DecalPrototype>>? OnDecalSelected;
|
||||
public Action<Color?>? OnColorChanged;
|
||||
public Action<int>? OnAngleChanged;
|
||||
public Action<bool>? OnSnapChanged;
|
||||
|
||||
private SpriteSystem? _sprite;
|
||||
private string _selectedDecal = string.Empty;
|
||||
private List<SprayPainterDecalEntry> _decals = [];
|
||||
|
||||
public SprayPainterDecals()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90;
|
||||
SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90;
|
||||
SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
|
||||
AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
|
||||
|
||||
UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
|
||||
SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
|
||||
ColorSelector.OnColorChanged += OnColorSelected;
|
||||
}
|
||||
|
||||
private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
|
||||
{
|
||||
OnColorChanged?.Invoke(UseCustomColorCheckBox.Pressed ? ColorSelector.Color : null);
|
||||
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
|
||||
}
|
||||
|
||||
private void SnapToTileCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
|
||||
{
|
||||
OnSnapChanged?.Invoke(SnapToTileCheckBox.Pressed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the decal list.
|
||||
/// </summary>
|
||||
public void PopulateDecals(List<SprayPainterDecalEntry> decals, SpriteSystem sprite)
|
||||
{
|
||||
_sprite ??= sprite;
|
||||
|
||||
_decals = decals;
|
||||
DecalsGrid.Children.Clear();
|
||||
|
||||
foreach (var decal in decals)
|
||||
{
|
||||
var button = new TextureButton()
|
||||
{
|
||||
TextureNormal = sprite.Frame0(decal.Sprite),
|
||||
Name = decal.Name,
|
||||
ToolTip = decal.Name,
|
||||
Scale = new Vector2(2, 2),
|
||||
};
|
||||
button.OnPressed += DecalButtonOnPressed;
|
||||
|
||||
if (UseCustomColorCheckBox.Pressed)
|
||||
{
|
||||
button.Modulate = ColorSelector.Color;
|
||||
}
|
||||
|
||||
if (_selectedDecal == decal.Name)
|
||||
{
|
||||
var panelContainer = new PanelContainer()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat()
|
||||
{
|
||||
BackgroundColor = StyleNano.ButtonColorDefault,
|
||||
},
|
||||
Children =
|
||||
{
|
||||
button,
|
||||
},
|
||||
};
|
||||
DecalsGrid.AddChild(panelContainer);
|
||||
}
|
||||
else
|
||||
{
|
||||
DecalsGrid.AddChild(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnColorSelected(Color color)
|
||||
{
|
||||
if (!UseCustomColorCheckBox.Pressed)
|
||||
return;
|
||||
|
||||
OnColorChanged?.Invoke(color);
|
||||
|
||||
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
|
||||
}
|
||||
|
||||
private void UpdateColorButtons(bool apply)
|
||||
{
|
||||
Color modulateColor = apply ? ColorSelector.Color : Color.White;
|
||||
foreach (var button in DecalsGrid.Children)
|
||||
{
|
||||
switch (button)
|
||||
{
|
||||
case TextureButton:
|
||||
button.Modulate = modulateColor;
|
||||
break;
|
||||
case PanelContainer panelContainer:
|
||||
{
|
||||
foreach (TextureButton textureButton in panelContainer.Children)
|
||||
textureButton.Modulate = modulateColor;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DecalButtonOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (obj.Button.Name is not { } name)
|
||||
return;
|
||||
|
||||
_selectedDecal = name;
|
||||
OnDecalSelected?.Invoke(_selectedDecal);
|
||||
|
||||
if (_sprite is null)
|
||||
return;
|
||||
|
||||
PopulateDecals(_decals, _sprite);
|
||||
}
|
||||
|
||||
public void SetSelectedDecal(string name)
|
||||
{
|
||||
_selectedDecal = name;
|
||||
|
||||
if (_sprite is null)
|
||||
return;
|
||||
|
||||
PopulateDecals(_decals, _sprite);
|
||||
}
|
||||
|
||||
public void SetAngle(int degrees)
|
||||
{
|
||||
AngleSpinBox.OverrideValue(degrees);
|
||||
}
|
||||
|
||||
public void SetColor(Color? color)
|
||||
{
|
||||
UseCustomColorCheckBox.Pressed = color != null;
|
||||
if (color != null)
|
||||
ColorSelector.Color = color.Value;
|
||||
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
|
||||
}
|
||||
|
||||
public void SetSnap(bool snap)
|
||||
{
|
||||
SnapToTileCheckBox.Pressed = snap;
|
||||
}
|
||||
}
|
||||
12
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml
Normal file
12
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<BoxContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Orientation="Vertical">
|
||||
<Label Text="{Loc 'spray-painter-selected-style'}" />
|
||||
<controls:ListContainer
|
||||
Name="StyleList"
|
||||
Toggle="True"
|
||||
Group="True">
|
||||
<!-- populated by code -->
|
||||
</controls:ListContainer>
|
||||
</BoxContainer>
|
||||
66
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs
Normal file
66
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Used to display a group of paintable styles in the spray painter menu.
|
||||
/// (e.g. each type of paintable locker or plastic crate)
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SprayPainterGroup : BoxContainer
|
||||
{
|
||||
public event Action<SpriteListData>? OnButtonPressed;
|
||||
|
||||
public SprayPainterGroup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
StyleList.GenerateItem = GenerateItems;
|
||||
}
|
||||
|
||||
public void PopulateList(List<SpriteListData> spriteList)
|
||||
{
|
||||
StyleList.PopulateList(spriteList);
|
||||
}
|
||||
|
||||
public void SelectItemByStyle(string key)
|
||||
{
|
||||
foreach (var elem in StyleList.Data)
|
||||
{
|
||||
if (elem is not SpriteListData spriteElem)
|
||||
continue;
|
||||
|
||||
if (spriteElem.Style == key)
|
||||
{
|
||||
StyleList.Select(spriteElem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateItems(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not SpriteListData spriteListData)
|
||||
return;
|
||||
|
||||
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
|
||||
var protoView = new EntityPrototypeView();
|
||||
protoView.SetPrototype(spriteListData.Prototype);
|
||||
var label = new Label()
|
||||
{
|
||||
Text = Loc.GetString($"spray-painter-style-{spriteListData.Group.ToLower()}-{spriteListData.Style.ToLower()}")
|
||||
};
|
||||
|
||||
box.AddChild(protoView);
|
||||
box.AddChild(label);
|
||||
button.AddChild(box);
|
||||
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
|
||||
button.OnPressed += _ => OnButtonPressed?.Invoke(spriteListData);
|
||||
|
||||
if (spriteListData.SelectedIndex == button.Index)
|
||||
button.Pressed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,6 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
MinSize="500 300"
|
||||
SetSize="500 500"
|
||||
Title="{Loc 'spray-painter-window-title'}">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="4"
|
||||
MinWidth="450">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="4"
|
||||
MinWidth="200">
|
||||
<Label Name="SelectedSpriteLabel"
|
||||
Text="{Loc 'spray-painter-selected-style'}">
|
||||
</Label>
|
||||
<ItemList Name="SpriteList"
|
||||
SizeFlagsStretchRatio="8"
|
||||
VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="4"
|
||||
MinWidth="200">
|
||||
<Label Name="SelectedColorLabel"
|
||||
Text="{Loc 'spray-painter-selected-color'}"/>
|
||||
<ItemList Name="ColorList"
|
||||
SizeFlagsStretchRatio="8"
|
||||
VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
MinSize="520 300"
|
||||
SetSize="520 700"
|
||||
Title="{Loc 'spray-painter-window-title'}">
|
||||
<TabContainer Name="Tabs"/>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A window to select spray painter settings by object type, as well as pipe colours and decals.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
{
|
||||
@@ -15,13 +22,33 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
public Action<ItemList.ItemListSelectedEventArgs>? OnSpritePicked;
|
||||
public Action<ItemList.ItemListSelectedEventArgs>? OnColorPicked;
|
||||
// Events
|
||||
public event Action<string, string>? OnSpritePicked;
|
||||
public event Action<int, bool>? OnTabChanged;
|
||||
public event Action<ProtoId<DecalPrototype>>? OnDecalChanged;
|
||||
public event Action<ItemList.ItemListSelectedEventArgs>? OnSetPipeColor;
|
||||
public event Action<Color?>? OnDecalColorChanged;
|
||||
public event Action<int>? OnDecalAngleChanged;
|
||||
public event Action<bool>? OnDecalSnapChanged;
|
||||
|
||||
// Pipe color data
|
||||
private ItemList _colorList = default!;
|
||||
public Dictionary<string, int> ItemColorIndex = new();
|
||||
|
||||
private Dictionary<string, Color> currentPalette = new();
|
||||
private const string colorLocKeyPrefix = "pipe-painter-color-";
|
||||
private List<SprayPainterEntry> CurrentEntries = new List<SprayPainterEntry>();
|
||||
private Dictionary<string, Color> _currentPalette = new();
|
||||
private const string ColorLocKeyPrefix = "pipe-painter-color-";
|
||||
|
||||
// Paintable objects
|
||||
private Dictionary<string, Dictionary<string, EntProtoId>> _currentStylesByGroup = new();
|
||||
private Dictionary<string, List<string>> _currentGroupsByCategory = new();
|
||||
|
||||
// Tab controls
|
||||
private Dictionary<string, SprayPainterGroup> _paintableControls = new();
|
||||
private BoxContainer? _pipeControl;
|
||||
|
||||
// Decals
|
||||
private List<SprayPainterDecalEntry> _currentDecals = [];
|
||||
private SprayPainterDecals? _sprayPainterDecals;
|
||||
|
||||
private readonly SpriteSpecifier _colorEntryIconTexture = new SpriteSpecifier.Rsi(
|
||||
new ResPath("Structures/Piping/Atmospherics/pipe.rsi"),
|
||||
@@ -32,13 +59,14 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_spriteSystem = _sysMan.GetEntitySystem<SpriteSystem>();
|
||||
Tabs.OnTabChanged += (index) => OnTabChanged?.Invoke(index, _sprayPainterDecals?.GetPositionInParent() == index);
|
||||
}
|
||||
|
||||
private string GetColorLocString(string? colorKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(colorKey))
|
||||
return Loc.GetString("pipe-painter-no-color-selected");
|
||||
var locKey = colorLocKeyPrefix + colorKey;
|
||||
var locKey = ColorLocKeyPrefix + colorKey;
|
||||
|
||||
if (!_loc.TryGetString(locKey, out var locString))
|
||||
locString = colorKey;
|
||||
@@ -48,51 +76,229 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
|
||||
public string? IndexToColorKey(int index)
|
||||
{
|
||||
return (string?) ColorList[index].Metadata;
|
||||
return _colorList[index].Text;
|
||||
}
|
||||
|
||||
public void Populate(List<SprayPainterEntry> entries, int selectedStyle, string? selectedColorKey, Dictionary<string, Color> palette)
|
||||
private void OnStyleSelected(ListData data)
|
||||
{
|
||||
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
|
||||
if (!CurrentEntries.Equals(entries))
|
||||
if (data is SpriteListData listData)
|
||||
OnSpritePicked?.Invoke(listData.Group, listData.Style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper to allow for selecting/deselecting the event to avoid loops
|
||||
/// </summary>
|
||||
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
OnSetPipeColor?.Invoke(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup function for the window.
|
||||
/// </summary>
|
||||
/// <param name="stylesByGroup">Each group, mapped by name to the set of named styles by their associated entity prototype.</param>
|
||||
/// <param name="groupsByCategory">The set of categories and the groups associated with them.</param>
|
||||
/// <param name="decals">A list of each decal.</param>
|
||||
public void PopulateCategories(Dictionary<string, Dictionary<string, EntProtoId>> stylesByGroup, Dictionary<string, List<string>> groupsByCategory, List<SprayPainterDecalEntry> decals)
|
||||
{
|
||||
bool tabsCleared = false;
|
||||
var lastTab = Tabs.CurrentTab;
|
||||
|
||||
if (!_currentGroupsByCategory.Equals(groupsByCategory))
|
||||
{
|
||||
CurrentEntries = entries;
|
||||
SpriteList.Clear();
|
||||
foreach (var entry in entries)
|
||||
// Destroy all existing tabs
|
||||
tabsCleared = true;
|
||||
_paintableControls.Clear();
|
||||
_pipeControl = null;
|
||||
_sprayPainterDecals = null;
|
||||
Tabs.RemoveAllChildren();
|
||||
}
|
||||
|
||||
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
|
||||
if (tabsCleared || !_currentStylesByGroup.Equals(stylesByGroup))
|
||||
{
|
||||
_currentStylesByGroup = stylesByGroup;
|
||||
|
||||
var tabIndex = 0;
|
||||
foreach (var (categoryName, categoryGroups) in groupsByCategory.OrderBy(c => c.Key))
|
||||
{
|
||||
SpriteList.AddItem(entry.Name, entry.Icon);
|
||||
if (categoryGroups.Count <= 0)
|
||||
continue;
|
||||
|
||||
// Repopulating controls:
|
||||
// ensure that categories with multiple groups have separate subtabs
|
||||
// but single-group categories do not.
|
||||
if (tabsCleared)
|
||||
{
|
||||
TabContainer? subTabs = null;
|
||||
if (categoryGroups.Count > 1)
|
||||
subTabs = new();
|
||||
|
||||
foreach (var group in categoryGroups)
|
||||
{
|
||||
if (!stylesByGroup.TryGetValue(group, out var styles))
|
||||
continue;
|
||||
|
||||
var groupControl = new SprayPainterGroup();
|
||||
groupControl.OnButtonPressed += OnStyleSelected;
|
||||
_paintableControls[group] = groupControl;
|
||||
if (categoryGroups.Count > 1)
|
||||
{
|
||||
if (subTabs != null)
|
||||
{
|
||||
subTabs?.AddChild(groupControl);
|
||||
var subTabLocalization = Loc.GetString("spray-painter-tab-group-" + group.ToLower());
|
||||
TabContainer.SetTabTitle(groupControl, subTabLocalization);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Tabs.AddChild(groupControl);
|
||||
}
|
||||
}
|
||||
|
||||
if (subTabs != null)
|
||||
Tabs.AddChild(subTabs);
|
||||
|
||||
var tabLocalization = Loc.GetString("spray-painter-tab-category-" + categoryName.ToLower());
|
||||
Tabs.SetTabTitle(tabIndex, tabLocalization);
|
||||
tabIndex++;
|
||||
}
|
||||
|
||||
// Finally, populate all groups with new data.
|
||||
foreach (var group in categoryGroups)
|
||||
{
|
||||
if (!stylesByGroup.TryGetValue(group, out var styles) ||
|
||||
!_paintableControls.TryGetValue(group, out var control))
|
||||
continue;
|
||||
|
||||
var dataList = styles
|
||||
.Select(e => new SpriteListData(group, e.Key, e.Value, 0))
|
||||
.OrderBy(d => Loc.GetString($"spray-painter-style-{group.ToLower()}-{d.Style.ToLower()}"))
|
||||
.ToList();
|
||||
control.PopulateList(dataList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentPalette.Equals(palette))
|
||||
{
|
||||
currentPalette = palette;
|
||||
ItemColorIndex.Clear();
|
||||
ColorList.Clear();
|
||||
PopulateColors(_currentPalette);
|
||||
|
||||
if (!_currentDecals.Equals(decals))
|
||||
{
|
||||
_currentDecals = decals;
|
||||
|
||||
if (_sprayPainterDecals is null)
|
||||
{
|
||||
_sprayPainterDecals = new SprayPainterDecals();
|
||||
|
||||
_sprayPainterDecals.OnDecalSelected += id => OnDecalChanged?.Invoke(id);
|
||||
_sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
|
||||
_sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
|
||||
_sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
|
||||
|
||||
Tabs.AddChild(_sprayPainterDecals);
|
||||
TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
|
||||
}
|
||||
|
||||
_sprayPainterDecals.PopulateDecals(decals, _spriteSystem);
|
||||
}
|
||||
|
||||
if (tabsCleared)
|
||||
SetSelectedTab(lastTab);
|
||||
}
|
||||
|
||||
public void PopulateColors(Dictionary<string, Color> palette)
|
||||
{
|
||||
// Create pipe tab controls if they don't exist
|
||||
bool tabCreated = false;
|
||||
if (_pipeControl == null)
|
||||
{
|
||||
_pipeControl = new BoxContainer() { Orientation = BoxContainer.LayoutOrientation.Vertical };
|
||||
|
||||
var label = new Label() { Text = Loc.GetString("spray-painter-selected-color") };
|
||||
|
||||
_colorList = new ItemList() { VerticalExpand = true };
|
||||
_colorList.OnItemSelected += OnColorPicked;
|
||||
|
||||
_pipeControl.AddChild(label);
|
||||
_pipeControl.AddChild(_colorList);
|
||||
|
||||
Tabs.AddChild(_pipeControl);
|
||||
TabContainer.SetTabTitle(_pipeControl, Loc.GetString("spray-painter-tab-category-pipes"));
|
||||
tabCreated = true;
|
||||
}
|
||||
|
||||
// Populate the tab if needed (new tab/new data)
|
||||
if (tabCreated || !_currentPalette.Equals(palette))
|
||||
{
|
||||
_currentPalette = palette;
|
||||
ItemColorIndex.Clear();
|
||||
_colorList.Clear();
|
||||
|
||||
int index = 0;
|
||||
foreach (var color in palette)
|
||||
{
|
||||
var locString = GetColorLocString(color.Key);
|
||||
var item = ColorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture));
|
||||
var item = _colorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture), metadata: color.Key);
|
||||
item.IconModulate = color.Value;
|
||||
item.Metadata = color.Key;
|
||||
|
||||
ItemColorIndex.Add(color.Key, ColorList.IndexOf(item));
|
||||
ItemColorIndex.Add(color.Key, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable event so we don't send a new event for pre-selectedStyle entry and end up in a loop
|
||||
|
||||
if (selectedColorKey != null)
|
||||
{
|
||||
var index = ItemColorIndex[selectedColorKey];
|
||||
ColorList.OnItemSelected -= OnColorPicked;
|
||||
ColorList[index].Selected = true;
|
||||
ColorList.OnItemSelected += OnColorPicked;
|
||||
}
|
||||
|
||||
SpriteList.OnItemSelected -= OnSpritePicked;
|
||||
SpriteList[selectedStyle].Selected = true;
|
||||
SpriteList.OnItemSelected += OnSpritePicked;
|
||||
}
|
||||
|
||||
# region Setters
|
||||
public void SetSelectedStyles(Dictionary<string, string> selectedStyles)
|
||||
{
|
||||
foreach (var (group, style) in selectedStyles)
|
||||
{
|
||||
if (!_paintableControls.TryGetValue(group, out var control))
|
||||
continue;
|
||||
|
||||
control.SelectItemByStyle(style);
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectColor(string color)
|
||||
{
|
||||
if (_colorList != null && ItemColorIndex.TryGetValue(color, out var colorIdx))
|
||||
{
|
||||
_colorList.OnItemSelected -= OnColorPicked;
|
||||
_colorList[colorIdx].Selected = true;
|
||||
_colorList.OnItemSelected += OnColorPicked;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSelectedTab(int tab)
|
||||
{
|
||||
Tabs.CurrentTab = int.Min(tab, Tabs.ChildCount - 1);
|
||||
}
|
||||
|
||||
public void SetSelectedDecal(string decal)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetSelectedDecal(decal);
|
||||
}
|
||||
|
||||
public void SetDecalAngle(int angle)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetAngle(angle);
|
||||
}
|
||||
|
||||
public void SetDecalColor(Color? color)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetColor(color);
|
||||
}
|
||||
|
||||
public void SetDecalSnap(bool snap)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetSnap(snap);
|
||||
}
|
||||
# endregion
|
||||
}
|
||||
|
||||
public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.StatusEffectNew.Components;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.StatusEffectNew;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed partial class ClientStatusEffectsSystem : SharedStatusEffectsSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StatusEffectContainerComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<StatusEffectContainerComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not StatusEffectContainerComponentState state)
|
||||
return;
|
||||
|
||||
var toRemove = new ValueList<EntityUid>();
|
||||
foreach (var effect in ent.Comp.ActiveStatusEffects)
|
||||
{
|
||||
if (state.ActiveStatusEffects.Contains(GetNetEntity(effect)))
|
||||
continue;
|
||||
|
||||
toRemove.Add(effect);
|
||||
}
|
||||
|
||||
foreach (var effect in toRemove)
|
||||
{
|
||||
ent.Comp.ActiveStatusEffects.Remove(effect);
|
||||
var ev = new StatusEffectRemovedEvent(ent);
|
||||
RaiseLocalEvent(effect, ref ev);
|
||||
}
|
||||
|
||||
foreach (var effect in state.ActiveStatusEffects)
|
||||
{
|
||||
var effectUid = GetEntity(effect);
|
||||
if (ent.Comp.ActiveStatusEffects.Contains(effectUid))
|
||||
continue;
|
||||
|
||||
ent.Comp.ActiveStatusEffects.Add(effectUid);
|
||||
var ev = new StatusEffectAppliedEvent(ent);
|
||||
RaiseLocalEvent(effectUid, ref ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Storage.Visualizers;
|
||||
|
||||
public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStorageVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -26,12 +31,34 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
|
||||
SpriteSystem.LayerSetRsiState((uid, sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, EntityStorageVisualsComponent comp, ref AppearanceChangeEvent args)
|
||||
protected override void OnAppearanceChange(EntityUid uid,
|
||||
EntityStorageVisualsComponent comp,
|
||||
ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null
|
||||
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
|
||||
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
|
||||
return;
|
||||
|
||||
var forceRedrawBase = false;
|
||||
if (AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
|
||||
{
|
||||
if (_prototypeManager.TryIndex(prototype, out var proto))
|
||||
{
|
||||
if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
|
||||
{
|
||||
SpriteSystem.SetBaseRsi((uid, args.Sprite), sprite.BaseRSI);
|
||||
}
|
||||
if (proto.TryGetComponent(out EntityStorageVisualsComponent? visuals, _componentFactory))
|
||||
{
|
||||
comp.StateBaseOpen = visuals.StateBaseOpen;
|
||||
comp.StateBaseClosed = visuals.StateBaseClosed;
|
||||
comp.StateDoorOpen = visuals.StateDoorOpen;
|
||||
comp.StateDoorClosed = visuals.StateDoorClosed;
|
||||
forceRedrawBase = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open/Closed state for the storage entity.
|
||||
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), StorageVisualLayers.Door, out _, false))
|
||||
{
|
||||
@@ -52,6 +79,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
|
||||
|
||||
if (comp.StateBaseOpen != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
|
||||
else if (forceRedrawBase && comp.StateBaseClosed != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -68,6 +97,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
|
||||
|
||||
if (comp.StateBaseClosed != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
|
||||
else if (forceRedrawBase && comp.StateBaseOpen != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,182 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client.Stunnable
|
||||
namespace Content.Client.Stunnable;
|
||||
|
||||
public sealed class StunSystem : SharedStunSystem
|
||||
{
|
||||
public sealed class StunSystem : SharedStunSystem
|
||||
{
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
|
||||
|
||||
private readonly int[] _sign = [-1, 1];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
|
||||
|
||||
CommandBinds.Builder
|
||||
.BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem))
|
||||
.Register<StunSystem>();
|
||||
}
|
||||
|
||||
private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (args.Session?.AttachedEntity is not {Valid: true} uid)
|
||||
return false;
|
||||
|
||||
if (args.EntityUid != uid || !HasComp<KnockedDownComponent>(uid) || !_combat.IsInCombatMode(uid))
|
||||
return false;
|
||||
|
||||
RaisePredictiveEvent(new ForceStandUpEvent());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add stun visual layers
|
||||
/// </summary>
|
||||
private void OnComponentInit(Entity<StunVisualsComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
var spriteEntity = (entity.Owner, sprite);
|
||||
|
||||
_spriteSystem.LayerMapReserve(spriteEntity, StunVisualLayers.StamCrit);
|
||||
_spriteSystem.LayerSetVisible(spriteEntity, StunVisualLayers.StamCrit, false);
|
||||
_spriteSystem.LayerSetOffset(spriteEntity, StunVisualLayers.StamCrit, new Vector2(0, 0.3125f));
|
||||
|
||||
_spriteSystem.LayerSetRsi(spriteEntity, StunVisualLayers.StamCrit, entity.Comp.StarsPath);
|
||||
|
||||
UpdateAppearance((entity, sprite), entity.Comp.State);
|
||||
}
|
||||
|
||||
private void OnAppearanceChanged(Entity<StunVisualsComponent> entity, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite != null)
|
||||
UpdateAppearance((entity, args.Sprite), entity.Comp.State);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(Entity<SpriteComponent?> entity, string state)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
if (!_spriteSystem.LayerMapTryGet((entity, entity.Comp), StunVisualLayers.StamCrit, out var index, false))
|
||||
return;
|
||||
|
||||
var visible = Appearance.TryGetData<bool>(entity, StunVisuals.SeeingStars, out var stars) && stars;
|
||||
|
||||
_spriteSystem.LayerSetVisible((entity, entity.Comp), index, visible);
|
||||
_spriteSystem.LayerSetRsiState((entity, entity.Comp), index, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple fatigue animation, a mild modification of the jittering animation. The animation constructor is
|
||||
/// quite complex, but that's because the AnimationSystem doesn't have proper adjustment layers. In a potential
|
||||
/// future where proper adjustment layers are added feel free to clean this up to be an animation with two adjustment
|
||||
/// layers rather than one mega layer.
|
||||
/// </summary>
|
||||
/// <param name="sprite">The spriteComponent we're adjusting the offset of</param>
|
||||
/// <param name="frequency">How many times per second does the animation run?</param>
|
||||
/// <param name="jitters">How many times should we jitter during the animation? Also determines breathing frequency</param>
|
||||
/// <param name="minJitter">Mininum jitter offset multiplier for X and Y directions</param>
|
||||
/// <param name="maxJitter">Maximum jitter offset multiplier for X and Y directions</param>
|
||||
/// <param name="breathing">Maximum breathing offset, this is in the Y direction</param>
|
||||
/// <param name="startOffset">Starting offset because we don't have adjustment layers</param>
|
||||
/// <param name="lastJitter">Last jitter so we don't jitter to the same quadrant</param>
|
||||
/// <returns></returns>
|
||||
public Animation GetFatigueAnimation(SpriteComponent sprite,
|
||||
float frequency,
|
||||
int jitters,
|
||||
Vector2 minJitter,
|
||||
Vector2 maxJitter,
|
||||
float breathing,
|
||||
Vector2 startOffset,
|
||||
ref Vector2 lastJitter)
|
||||
{
|
||||
// avoid animations with negative length or infinite length
|
||||
if (frequency <= 0)
|
||||
return new Animation();
|
||||
|
||||
var breaths = new Vector2(0, breathing * 2) / jitters;
|
||||
|
||||
var length = 1 / frequency;
|
||||
var frames = length / jitters;
|
||||
|
||||
var keyFrames = new List<AnimationTrackProperty.KeyFrame> { new(sprite.Offset, 0f) };
|
||||
|
||||
// Spits out a list of keyframes to feed to the AnimationPlayer based on the variables we've inputted
|
||||
for (var i = 1; i <= jitters; i++)
|
||||
{
|
||||
var offset = new Vector2(_random.NextFloat(minJitter.X, maxJitter.X),
|
||||
_random.NextFloat(minJitter.Y, maxJitter.Y));
|
||||
offset.X *= _random.Pick(_sign);
|
||||
offset.Y *= _random.Pick(_sign);
|
||||
|
||||
if (i == 1 && Math.Sign(offset.X) == Math.Sign(lastJitter.X)
|
||||
&& Math.Sign(offset.Y) == Math.Sign(lastJitter.Y))
|
||||
{
|
||||
// If the sign is the same as last time on both axis we flip one randomly
|
||||
// to avoid jitter staying in one quadrant too much.
|
||||
if (_random.Prob(0.5f))
|
||||
offset.X *= -1;
|
||||
else
|
||||
offset.Y *= -1;
|
||||
}
|
||||
|
||||
lastJitter = offset;
|
||||
|
||||
// For the first half of the jitter, we vertically displace the sprite upwards to simulate breathing in
|
||||
if (i <= jitters / 2)
|
||||
{
|
||||
keyFrames.Add(new AnimationTrackProperty.KeyFrame(startOffset + breaths * i + offset, frames));
|
||||
}
|
||||
// For the next quarter we displace the sprite down, to about 12.5% breathing offset below our starting position
|
||||
// Simulates breathing out
|
||||
else if (i < jitters * 3 / 4)
|
||||
{
|
||||
keyFrames.Add(
|
||||
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( jitters - i * 1.5f ) + offset, frames));
|
||||
}
|
||||
// Return to our starting position for breathing, jitter reaches its final position
|
||||
else
|
||||
{
|
||||
keyFrames.Add(
|
||||
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( i - jitters ) + offset, frames));
|
||||
}
|
||||
}
|
||||
|
||||
return new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(length),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty
|
||||
{
|
||||
// Heavy Breathing
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Offset),
|
||||
InterpolationMode = AnimationInterpolationMode.Cubic,
|
||||
KeyFrames = keyFrames,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum StunVisualLayers : byte
|
||||
{
|
||||
StamCrit,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Mobs;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
@@ -43,7 +44,7 @@ public sealed class ProgressColorSystem : EntitySystem
|
||||
|
||||
// lerp
|
||||
var hue = 5f / 18f * progress;
|
||||
return Color.FromHsv((hue, 1f, 0.75f, 1f));
|
||||
return Color.FromHsv(new Vector4(hue, 1f, 0.75f, 1f));
|
||||
}
|
||||
|
||||
return InterpolateColorGaussian(Plasma, progress);
|
||||
|
||||
@@ -185,7 +185,12 @@ public sealed class ItemGridPiece : Control, IEntityControl
|
||||
|
||||
handle.SetTransform(pos, iconRotation);
|
||||
var box = new UIBox2(root, root + sprite.Size * scale);
|
||||
handle.DrawTextureRect(sprite, box);
|
||||
|
||||
var ev = new BeforeRenderInGridEvent(new Color(255, 255, 255));
|
||||
_entityManager.EventBus.RaiseLocalEvent(Entity, ev);
|
||||
|
||||
handle.DrawTextureRect(sprite, box, ev.Color);
|
||||
|
||||
handle.SetTransform(GlobalPixelPosition, Angle.Zero);
|
||||
}
|
||||
else
|
||||
@@ -298,6 +303,19 @@ public sealed class ItemGridPiece : Control, IEntityControl
|
||||
public EntityUid? UiEntity => Entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This event gets raised before a sprite gets drawn in a grid and lets to change the sprite color for several gameobjects that have special sprites to render in containers.
|
||||
/// </summary>
|
||||
public sealed class BeforeRenderInGridEvent : EntityEventArgs
|
||||
{
|
||||
public Color Color { get; set; }
|
||||
|
||||
public BeforeRenderInGridEvent(Color color)
|
||||
{
|
||||
Color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ItemGridPieceMarks
|
||||
{
|
||||
First,
|
||||
|
||||
@@ -621,7 +621,7 @@ public sealed class StorageWindow : BaseWindow
|
||||
{
|
||||
marked.Add(cell);
|
||||
cell.ModulateSelfOverride = spotFree
|
||||
? Color.FromHsv((0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
|
||||
? Color.FromHsv(new Vector4(0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
|
||||
: Color.FromHex("#2222CC");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Content.IntegrationTests.Tests
|
||||
}
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(15);
|
||||
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
@@ -112,7 +112,7 @@ namespace Content.IntegrationTests.Tests
|
||||
entityMan.SpawnEntity(protoId, map.GridCoords);
|
||||
}
|
||||
});
|
||||
await server.WaitRunTicks(15);
|
||||
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan)
|
||||
@@ -270,7 +270,7 @@ namespace Content.IntegrationTests.Tests
|
||||
await pair.RunTicksSync(3);
|
||||
|
||||
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
|
||||
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
|
||||
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
|
||||
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>);
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -17,9 +17,7 @@ namespace Content.IntegrationTests.Tests
|
||||
components:
|
||||
- type: Inventory
|
||||
- type: ContainerContainer
|
||||
- type: StatusEffects
|
||||
allowed:
|
||||
- Stun
|
||||
- type: MobState
|
||||
|
||||
- type: entity
|
||||
name: InventoryJumpsuitJanitorDummy
|
||||
@@ -70,7 +68,7 @@ namespace Content.IntegrationTests.Tests
|
||||
});
|
||||
#pragma warning restore NUnit2045
|
||||
|
||||
systemMan.GetEntitySystem<StunSystem>().TryStun(human, TimeSpan.FromSeconds(1f), true);
|
||||
systemMan.GetEntitySystem<StunSystem>().TryUpdateStunDuration(human, TimeSpan.FromSeconds(1f));
|
||||
|
||||
#pragma warning disable NUnit2045
|
||||
// Since the mob is stunned, they can't equip this.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
@@ -339,7 +339,7 @@ public sealed partial class MindTests
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<SharedMindSystem>();
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
EntityUid entity = default!;
|
||||
EntityUid mindId = default!;
|
||||
@@ -379,7 +379,7 @@ public sealed partial class MindTests
|
||||
|
||||
mob = entMan.SpawnEntity(null, new MapCoordinates());
|
||||
|
||||
MakeSentientCommand.MakeSentient(mob, entMan);
|
||||
mindSystem.MakeSentient(mob);
|
||||
mobMindId = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second");
|
||||
mobMind = entMan.GetComponent<MindComponent>(mobMindId);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Threading;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -22,7 +21,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
private void OnRoundEnd(RoundEndSystemChangedEvent ev)
|
||||
{
|
||||
Interlocked.Increment(ref RoundCount);
|
||||
RoundCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +126,17 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
async Task WaitForEvent()
|
||||
{
|
||||
var timeout = Task.Delay(TimeSpan.FromSeconds(10));
|
||||
var currentCount = Thread.VolatileRead(ref sys.RoundCount);
|
||||
while (currentCount == Thread.VolatileRead(ref sys.RoundCount) && !timeout.IsCompleted)
|
||||
const int maxTicks = 60;
|
||||
var currentCount = sys.RoundCount;
|
||||
for (var i = 0; i < maxTicks; i++)
|
||||
{
|
||||
await pair.RunTicksSync(5);
|
||||
if (currentCount != sys.RoundCount)
|
||||
return;
|
||||
|
||||
await pair.RunTicksSync(1);
|
||||
}
|
||||
if (timeout.IsCompleted) throw new TimeoutException("Event took too long to trigger");
|
||||
|
||||
throw new TimeoutException("Event took too long to trigger");
|
||||
}
|
||||
|
||||
// Need to clean self up
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.SmartFridge;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeInteractionTest : InteractionTest
|
||||
{
|
||||
private const string SmartFridgeProtoId = "SmartFridge";
|
||||
private const string SampleItemProtoId = "FoodAmbrosiaVulgaris";
|
||||
private const string SampleDumpableAndInsertableId = "PillCanisterSomething";
|
||||
private const int SampleDumpableCount = 5;
|
||||
private const string SampleDumpableId = "ChemBagSomething";
|
||||
|
||||
[TestPrototypes]
|
||||
private const string TestPrototypes = $@"
|
||||
- type: entity
|
||||
parent: PillCanister
|
||||
id: {SampleDumpableAndInsertableId}
|
||||
components:
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
|
||||
- type: entity
|
||||
parent: ChemBag
|
||||
id: {SampleDumpableId}
|
||||
components:
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
";
|
||||
|
||||
[Test]
|
||||
public async Task InsertAndDispenseItemTest()
|
||||
{
|
||||
await PlaceInHands(SampleItemProtoId);
|
||||
|
||||
await SpawnTarget(SmartFridgeProtoId);
|
||||
var fridge = SEntMan.GetEntity(Target.Value);
|
||||
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
|
||||
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
// smartfridge spawns with nothing
|
||||
Assert.That(component.Entries, Is.Empty);
|
||||
await InteractUsing(SampleItemProtoId);
|
||||
|
||||
// smartfridge now has items
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]], Is.Not.Empty);
|
||||
|
||||
// open the UI
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(SmartFridgeUiKey.Key));
|
||||
|
||||
// dispense an item
|
||||
await SendBui(SmartFridgeUiKey.Key, new SmartFridgeDispenseItemMessage(component.Entries[0]));
|
||||
|
||||
// assert that the listing is still there
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
// but empty
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]], Is.Empty);
|
||||
|
||||
// and that the thing we dispensed is actually around
|
||||
await AssertEntityLookup(
|
||||
("APCBasic", 1),
|
||||
(SampleItemProtoId, 1)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InsertDumpableInsertableItemTest()
|
||||
{
|
||||
await PlaceInHands(SampleItemProtoId);
|
||||
|
||||
await SpawnTarget(SmartFridgeProtoId);
|
||||
var fridge = SEntMan.GetEntity(Target.Value);
|
||||
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
|
||||
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
await InteractUsing(SampleDumpableAndInsertableId);
|
||||
|
||||
// smartfridge now has one item only
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InsertDumpableItemTest()
|
||||
{
|
||||
await PlaceInHands(SampleItemProtoId);
|
||||
|
||||
await SpawnTarget(SmartFridgeProtoId);
|
||||
var fridge = SEntMan.GetEntity(Target.Value);
|
||||
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
|
||||
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
await InteractUsing(SampleDumpableId);
|
||||
|
||||
// smartfridge now has N items
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(SampleDumpableCount));
|
||||
}
|
||||
}
|
||||
58
Content.Server/Administration/Commands/CameraCommand.cs
Normal file
58
Content.Server/Administration/Commands/CameraCommand.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Content.Server.Administration.UI;
|
||||
using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class CameraCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly EuiManager _eui = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override string Command => "camera";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (shell.Player is not { } user)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var targetNetId) || !_entManager.TryGetEntity(targetNetId, out var targetUid))
|
||||
{
|
||||
if (!_playerManager.TryGetSessionByUsername(args[0], out var player)
|
||||
|| player.AttachedEntity == null)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-camera-wrong-argument"));
|
||||
return;
|
||||
}
|
||||
targetUid = player.AttachedEntity.Value;
|
||||
}
|
||||
|
||||
var ui = new AdminCameraEui(targetUid.Value);
|
||||
_eui.OpenEui(ui, user);
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
CompletionHelper.SessionNames(players: _playerManager),
|
||||
Loc.GetString("cmd-camera-hint"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Tabletop.Components;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
|
||||
if (!hadSlipComponent)
|
||||
{
|
||||
slipComponent.SlipData.SuperSlippery = true;
|
||||
slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5);
|
||||
slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
|
||||
slipComponent.SlipData.LaunchForwardsMultiplier = 20;
|
||||
}
|
||||
|
||||
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
|
||||
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
|
||||
};
|
||||
args.Verbs.Add(omniaccent);
|
||||
|
||||
var crawlerName = Loc.GetString("admin-smite-crawler-name").ToLowerInvariant();
|
||||
Verb crawler = new()
|
||||
{
|
||||
Text = crawlerName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Mobs/Animals/snake.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<WormComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
|
||||
};
|
||||
args.Verbs.Add(crawler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Content.Server.Administration.UI;
|
||||
using Content.Server.Disposal.Tube;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Prayer;
|
||||
using Content.Server.Silicons.Laws;
|
||||
@@ -16,7 +15,6 @@ using Content.Shared.Configurable;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Movement.Components;
|
||||
@@ -390,6 +388,22 @@ namespace Content.Server.Administration.Systems
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
|
||||
});
|
||||
}
|
||||
|
||||
// open camera
|
||||
args.Verbs.Add(new Verb()
|
||||
{
|
||||
Priority = 10,
|
||||
Text = Loc.GetString("admin-verbs-camera"),
|
||||
Message = Loc.GetString("admin-verbs-camera-description"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
|
||||
Category = VerbCategory.Admin,
|
||||
Act = () =>
|
||||
{
|
||||
var ui = new AdminCameraEui(args.Target);
|
||||
_euiManager.OpenEui(ui, player);
|
||||
},
|
||||
Impact = LogImpact.Low
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,7 +472,7 @@ namespace Content.Server.Administration.Systems
|
||||
Text = Loc.GetString("make-sentient-verb-get-data-text"),
|
||||
Category = VerbCategory.Debug,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/sentient.svg.192dpi.png")),
|
||||
Act = () => MakeSentientCommand.MakeSentient(args.Target, EntityManager),
|
||||
Act = () => _mindSystem.MakeSentient(args.Target),
|
||||
Impact = LogImpact.Medium
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
|
||||
97
Content.Server/Administration/UI/AdminCameraEui.cs
Normal file
97
Content.Server/Administration/UI/AdminCameraEui.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.Follower;
|
||||
using Content.Shared.Coordinates;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Administration.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Admin Eui for opening a viewport window to observe entities.
|
||||
/// Use the "Open Camera" admin verb or the "camera" command to open.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed partial class AdminCameraEui : BaseEui
|
||||
{
|
||||
[Dependency] private readonly IAdminManager _admin = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private readonly FollowerSystem _follower = default!;
|
||||
private readonly PvsOverrideSystem _pvs = default!;
|
||||
private readonly SharedViewSubscriberSystem _viewSubscriber = default!;
|
||||
|
||||
private static readonly EntProtoId CameraProtoId = "AdminCamera";
|
||||
|
||||
private readonly EntityUid _target;
|
||||
private EntityUid? _camera;
|
||||
|
||||
|
||||
public AdminCameraEui(EntityUid target)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_follower = _entityManager.System<FollowerSystem>();
|
||||
_pvs = _entityManager.System<PvsOverrideSystem>();
|
||||
_viewSubscriber = _entityManager.System<SharedViewSubscriberSystem>();
|
||||
|
||||
_target = target;
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
base.Opened();
|
||||
|
||||
_camera = CreateCamera(_target, Player);
|
||||
StateDirty();
|
||||
}
|
||||
|
||||
public override void Closed()
|
||||
{
|
||||
base.Closed();
|
||||
|
||||
_entityManager.DeleteEntity(_camera);
|
||||
}
|
||||
|
||||
public override void HandleMessage(EuiMessageBase msg)
|
||||
{
|
||||
base.HandleMessage(msg);
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case AdminCameraFollowMessage:
|
||||
if (!_admin.HasAdminFlag(Player, AdminFlags.Admin) || Player.AttachedEntity == null)
|
||||
return;
|
||||
_follower.StartFollowingEntity(Player.AttachedEntity.Value, _target);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override EuiStateBase GetNewState()
|
||||
{
|
||||
var name = _entityManager.GetComponent<MetaDataComponent>(_target).EntityName;
|
||||
var netEnt = _entityManager.GetNetEntity(_camera);
|
||||
return new AdminCameraEuiState(netEnt, name, _timing.CurTick);
|
||||
}
|
||||
|
||||
private EntityUid CreateCamera(EntityUid target, ICommonSession observer)
|
||||
{
|
||||
// Spawn a camera entity attached to the target.
|
||||
var coords = target.ToCoordinates();
|
||||
var camera = _entityManager.SpawnAttachedTo(CameraProtoId, coords);
|
||||
|
||||
// Allow the user to see the entities near the camera.
|
||||
// This also force sends the camera entity to the user, overriding the visibility flags.
|
||||
// (The camera entity has its visibility flags set to VisibilityFlags.Admin so that cheat clients can't see it)
|
||||
_viewSubscriber.AddViewSubscriber(camera, observer);
|
||||
|
||||
return camera;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ public sealed partial class ParrotMemoryComponent : Component
|
||||
/// The % chance an entity with this component learns a phrase when learning is off cooldown
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float LearnChance = 0.4f;
|
||||
public float LearnChance = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// Time after which another attempt can be made at learning a phrase
|
||||
|
||||
@@ -96,7 +96,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
|
||||
EntityManager.AddComponents(ent, injectedAnom.Components);
|
||||
|
||||
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
|
||||
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
|
||||
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
|
||||
|
||||
if (ent.Comp.StartSound is not null)
|
||||
@@ -125,7 +125,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
|
||||
private void OnAnomalyPulse(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyPulseEvent args)
|
||||
{
|
||||
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
|
||||
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity));
|
||||
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
|
||||
EntityManager.RemoveComponents(ent, injectedAnom.Components);
|
||||
|
||||
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
|
||||
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
|
||||
|
||||
if (ent.Comp.EndMessage is not null &&
|
||||
_mind.TryGetMind(ent, out _, out var mindComponent) &&
|
||||
|
||||
@@ -17,6 +17,7 @@ using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
|
||||
@@ -53,6 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
|
||||
// Grid events
|
||||
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
|
||||
SubscribeLocalEvent<PipeNodeGroupRemovedEvent>(OnPipeNodeGroupRemoved);
|
||||
}
|
||||
|
||||
#region Event handling
|
||||
@@ -295,6 +297,25 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
|
||||
#region Pipe net functions
|
||||
|
||||
private void OnPipeNodeGroupRemoved(ref PipeNodeGroupRemovedEvent args)
|
||||
{
|
||||
// When a pipe node group is removed, we need to iterate over all of
|
||||
// our pipe chunks and remove any entries with a matching net id.
|
||||
// (We only need to check the chunks for the affected grid, though.)
|
||||
|
||||
if (!_gridAtmosPipeChunks.TryGetValue(args.Grid, out var chunkData))
|
||||
return;
|
||||
|
||||
foreach (var chunk in chunkData.Values)
|
||||
{
|
||||
foreach (var key in chunk.AtmosPipeData.Keys)
|
||||
{
|
||||
if (key.NetId == args.NetId)
|
||||
chunk.AtmosPipeData.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
|
||||
{
|
||||
var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
|
||||
@@ -411,7 +432,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
continue;
|
||||
|
||||
var netId = GetPipeNodeNetId(pipeNode);
|
||||
var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color.ToHex());
|
||||
var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color);
|
||||
var pipeDirection = pipeNode.CurrentPipeDirection;
|
||||
|
||||
chunk.AtmosPipeData.TryGetValue(subnet, out var atmosPipeData);
|
||||
|
||||
@@ -279,6 +279,14 @@ public partial class AtmosphereSystem
|
||||
|
||||
public bool RemovePipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
|
||||
{
|
||||
// Technically this event can be fired even on grids that don't
|
||||
// actually have grid atmospheres.
|
||||
if (pipeNet.Grid is not null)
|
||||
{
|
||||
var ev = new PipeNodeGroupRemovedEvent(grid, pipeNet.NetId);
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
|
||||
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet);
|
||||
}
|
||||
|
||||
@@ -329,3 +337,12 @@ public partial class AtmosphereSystem
|
||||
[ByRefEvent] private record struct IsHotspotActiveMethodEvent
|
||||
(EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Raised broadcasted when a pipe node group within a grid has been removed.
|
||||
/// </summary>
|
||||
/// <param name="Grid">The grid with the removed node group.</param>
|
||||
/// <param name="NetId">The net id of the removed node group.</param>
|
||||
[ByRefEvent]
|
||||
public record struct PipeNodeGroupRemovedEvent(EntityUid Grid, int NetId);
|
||||
|
||||
@@ -596,8 +596,17 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
if (!reconsiderAdjacent)
|
||||
return;
|
||||
|
||||
// Before updating the adjacent tile flags that determine whether air is allowed to flow
|
||||
// or not, we explicitly update airtight data on these tiles right now.
|
||||
// This ensures that UpdateAdjacentTiles has updated data before updating flags.
|
||||
// This allows monstermos' floodfill check that determines if firelocks have dropped
|
||||
// to work correctly.
|
||||
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
|
||||
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, other);
|
||||
|
||||
UpdateAdjacentTiles(ent, tile);
|
||||
UpdateAdjacentTiles(ent, other);
|
||||
|
||||
InvalidateVisuals(ent, tile);
|
||||
InvalidateVisuals(ent, other);
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
flammable.Resisting = true;
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("flammable-component-resist-message"), uid, uid);
|
||||
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f), true);
|
||||
_stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(2f));
|
||||
|
||||
// TODO FLAMMABLE: Make this not use TimerComponent...
|
||||
uid.SpawnTimer(2000, () =>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
{
|
||||
public sealed class AtmosPipeColorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, AtmosPipeColorComponent component, ComponentStartup args)
|
||||
{
|
||||
if (!TryComp(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, PipeColorVisuals.Color, component.Color, appearance);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, AtmosPipeColorComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (!TryComp(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, PipeColorVisuals.Color, Color.White, appearance);
|
||||
}
|
||||
|
||||
public void SetColor(EntityUid uid, AtmosPipeColorComponent component, Color color)
|
||||
{
|
||||
component.Color = color;
|
||||
|
||||
if (!TryComp(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
|
||||
|
||||
var ev = new AtmosPipeColorChangedEvent(color);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
namespace Content.Server.Atmos
|
||||
{
|
||||
public abstract class PressureEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The environment pressure.
|
||||
/// </summary>
|
||||
public float Pressure { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The modifier for the apparent pressure.
|
||||
/// This number will be added to the environment pressure for calculation purposes.
|
||||
/// It can be negative to reduce the felt pressure, or positive to increase it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not set this directly. Add to it, or subtract from it to modify it.
|
||||
/// </remarks>
|
||||
public float Modifier { get; set; } = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// The multiplier for the apparent pressure.
|
||||
/// The environment pressure will be multiplied by this for calculation purposes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not set, add to or subtract from this directly. Multiply this by your multiplier only.
|
||||
/// </remarks>
|
||||
public float Multiplier { get; set; } = 1f;
|
||||
|
||||
protected PressureEvent(float pressure)
|
||||
{
|
||||
Pressure = pressure;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LowPressureEvent : PressureEvent
|
||||
{
|
||||
public LowPressureEvent(float pressure) : base(pressure) { }
|
||||
}
|
||||
|
||||
public sealed class HighPressureEvent : PressureEvent
|
||||
{
|
||||
public HighPressureEvent(float pressure) : base(pressure) { }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Body.Commands
|
||||
{
|
||||
@@ -15,9 +16,9 @@ namespace Content.Server.Body.Commands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private static readonly EntProtoId DefaultHandPrototype = "LeftHandHuman";
|
||||
private static int _handIdAccumulator;
|
||||
|
||||
public string Command => "addhand";
|
||||
public string Description => "Adds a hand to your entity.";
|
||||
@@ -114,9 +115,17 @@ namespace Content.Server.Body.Commands
|
||||
|
||||
if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.RootContainer.ContainedEntity == null)
|
||||
{
|
||||
var text = $"You have no body{(_random.Prob(0.2f) ? " and you must scream." : ".")}";
|
||||
var location = _entManager.GetComponentOrNull<BodyPartComponent>(hand)?.Symmetry switch
|
||||
{
|
||||
BodyPartSymmetry.None => HandLocation.Middle,
|
||||
BodyPartSymmetry.Left => HandLocation.Left,
|
||||
BodyPartSymmetry.Right => HandLocation.Right,
|
||||
_ => HandLocation.Right
|
||||
};
|
||||
_entManager.DeleteEntity(hand);
|
||||
|
||||
shell.WriteLine(text);
|
||||
// You have no body and you must scream.
|
||||
_entManager.System<HandsSystem>().AddHand(entity, $"{hand}-cmd-{_handIdAccumulator++}", location);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,6 @@ namespace Content.Server.Body.Components
|
||||
[RegisterComponent, Access(typeof(RespiratorSystem)), AutoGenerateComponentPause]
|
||||
public sealed partial class RespiratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Gas container for this entity
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public GasMixture Air = new()
|
||||
{
|
||||
Volume = 6, // 6 liters, the average lung capacity for a human according to Google
|
||||
Temperature = Atmospherics.NormalBodyTemperature
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Volume of our breath in liters
|
||||
/// </summary>
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class InternalsSystem : SharedInternalsSystem
|
||||
return;
|
||||
|
||||
// Could the entity metabolise the air in the linked gas tank?
|
||||
if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air))
|
||||
if (!_respirator.CanMetabolizeInhaledAir(uid, tank.Value.Comp.Air))
|
||||
return;
|
||||
|
||||
ToggleInternals(uid, uid, force: false, component, ToggleMode.On);
|
||||
|
||||
@@ -49,8 +49,13 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
UpdatesAfter.Add(typeof(MetabolizerSystem));
|
||||
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
|
||||
|
||||
// BodyComp stuff
|
||||
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
|
||||
SubscribeLocalEvent<BodyComponent, ExhaledGasEvent>(OnGasExhaled);
|
||||
SubscribeLocalEvent<BodyComponent, CanMetabolizeGasEvent>(CanBodyMetabolizeGas);
|
||||
SubscribeLocalEvent<BodyComponent, SuffocationEvent>(OnSuffocation);
|
||||
SubscribeLocalEvent<BodyComponent, StopSuffocatingEvent>(OnStopSuffocating);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
|
||||
@@ -80,11 +85,11 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
switch (respirator.Status)
|
||||
{
|
||||
case RespiratorStatus.Inhaling:
|
||||
Inhale(uid);
|
||||
Inhale((uid, respirator));
|
||||
respirator.Status = RespiratorStatus.Exhaling;
|
||||
break;
|
||||
case RespiratorStatus.Exhaling:
|
||||
Exhale(uid);
|
||||
Exhale((uid, respirator));
|
||||
respirator.Status = RespiratorStatus.Inhaling;
|
||||
break;
|
||||
}
|
||||
@@ -111,10 +116,10 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public bool Inhale(Entity<RespiratorComponent?> entity)
|
||||
public void Inhale(Entity<RespiratorComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, logMissing: false))
|
||||
return false;
|
||||
return;
|
||||
|
||||
// Inhale gas
|
||||
var ev = new InhaleLocationEvent
|
||||
@@ -126,23 +131,22 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
ev.Gas ??= _atmosSys.GetContainingMixture(entity.Owner, excite: true);
|
||||
|
||||
if (ev.Gas is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
|
||||
var gas = ev.Gas.RemoveVolume(entity.Comp.BreathVolume);
|
||||
|
||||
var inhaleEv = new InhaledGasEvent(gas);
|
||||
RaiseLocalEvent(entity, ref inhaleEv);
|
||||
|
||||
return inhaleEv.Handled && inhaleEv.Succeeded;
|
||||
}
|
||||
|
||||
public void Exhale(Entity<RespiratorComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, logMissing: false))
|
||||
if (inhaleEv.Handled && inhaleEv.Succeeded)
|
||||
return;
|
||||
|
||||
// If nothing could inhale the gas give it back.
|
||||
_atmosSys.Merge(ev.Gas, gas);
|
||||
}
|
||||
|
||||
public void Exhale(Entity<RespiratorComponent> entity)
|
||||
{
|
||||
// exhale gas
|
||||
|
||||
var ev = new ExhaleLocationEvent();
|
||||
@@ -157,8 +161,16 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
ev.Gas ??= GasMixture.SpaceGas;
|
||||
}
|
||||
|
||||
var exhaleEv = new ExhaledGasEvent(ev.Gas);
|
||||
RaiseLocalEvent(entity, ref exhaleEv);
|
||||
Exhale(entity!, ev.Gas);
|
||||
}
|
||||
|
||||
public void Exhale(Entity<RespiratorComponent?> entity, GasMixture gas)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, logMissing: false))
|
||||
return;
|
||||
|
||||
var ev = new ExhaledGasEvent(gas);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -176,52 +188,76 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
|
||||
/// gasses).
|
||||
/// Checks if it's safe for a given entity to breathe the air from the environment it is currently situated in.
|
||||
/// </summary>
|
||||
/// <param name="ent">The entity attempting to metabolize the gas.</param>
|
||||
/// <returns>Returns true only if the air is not toxic, and it wouldn't suffocate.</returns>
|
||||
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
if (!Inhale(ent))
|
||||
return false;
|
||||
// Get the gas at our location but don't actually remove it from the gas mixture.
|
||||
var ev = new InhaleLocationEvent
|
||||
{
|
||||
Respirator = ent.Comp,
|
||||
};
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
|
||||
// If we don't have a body we can't be poisoned by gas, yet...
|
||||
var success = TryMetabolizeGas((ent, ent.Comp));
|
||||
ev.Gas ??= _atmosSys.GetContainingMixture(ent.Owner, excite: true);
|
||||
|
||||
// Don't keep that gas in our lungs lest it poisons a poor nuclear operative.
|
||||
Exhale(ent);
|
||||
return success;
|
||||
// If there's no air to breathe or we can't metabolize it then internals should be on.
|
||||
return ev.Gas is not null && CanMetabolizeInhaledAir(ent, ev.Gas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
|
||||
/// (i.e., no toxic gasses).
|
||||
/// Checks if a given entity can safely metabolize a given gas mixture.
|
||||
/// </summary>
|
||||
public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
|
||||
/// <param name="ent">The entity attempting to metabolize the gas.</param>
|
||||
/// <param name="gas">The gas mixture we are trying to metabolize.</param>
|
||||
/// <returns>Returns true only if the gas mixture is not toxic, and it wouldn't suffocate.</returns>
|
||||
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent, GasMixture gas)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
if (organs.Count == 0)
|
||||
var ev = new CanMetabolizeGasEvent(gas);
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
|
||||
if (!ev.Handled || ev.Toxic)
|
||||
return false;
|
||||
|
||||
gas = new GasMixture(gas);
|
||||
var lungRatio = 1.0f / organs.Count;
|
||||
gas.Multiply(MathF.Min(lungRatio * gas.Volume / ent.Comp.BreathVolume, lungRatio));
|
||||
var solution = _lungSystem.GasToReagent(gas);
|
||||
return ev.Saturation > ent.Comp.UpdateInterval.TotalSeconds;
|
||||
}
|
||||
|
||||
float saturation = 0;
|
||||
/// <summary>
|
||||
/// Tries to safely metabolize the current solutions in a body's lungs.
|
||||
/// </summary>
|
||||
private void CanBodyMetabolizeGas(Entity<BodyComponent> ent, ref CanMetabolizeGasEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
if (organs.Count == 0)
|
||||
return;
|
||||
|
||||
var solution = _lungSystem.GasToReagent(args.Gas);
|
||||
|
||||
var saturation = 0f;
|
||||
foreach (var organ in organs)
|
||||
{
|
||||
saturation += GetSaturation(solution, organ.Owner, out var toxic);
|
||||
if (toxic)
|
||||
return false;
|
||||
if (!toxic)
|
||||
continue;
|
||||
|
||||
args.Handled = true;
|
||||
args.Toxic = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return saturation > ent.Comp.UpdateInterval.TotalSeconds;
|
||||
args.Handled = true;
|
||||
args.Saturation = saturation;
|
||||
}
|
||||
|
||||
public bool TryInhaleGasToBody(Entity<BodyComponent?> entity, GasMixture gas)
|
||||
@@ -265,30 +301,6 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
_atmosSys.Merge(gas, outGas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to safely metabolize the current solutions in a body's lungs.
|
||||
/// </summary>
|
||||
private bool TryMetabolizeGas(Entity<RespiratorComponent, BodyComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp2))
|
||||
return false;
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
if (organs.Count == 0)
|
||||
return false;
|
||||
|
||||
float saturation = 0;
|
||||
foreach (var organ in organs)
|
||||
{
|
||||
var solution = _lungSystem.GasToReagent(organ.Comp1.Air);
|
||||
saturation += GetSaturation(solution, organ.Owner, out var toxic);
|
||||
if (toxic)
|
||||
return false;
|
||||
}
|
||||
|
||||
return saturation > ent.Comp1.UpdateInterval.TotalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
|
||||
/// </summary>
|
||||
@@ -354,15 +366,11 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
|
||||
|
||||
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
|
||||
{
|
||||
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
foreach (var entity in organs)
|
||||
{
|
||||
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
|
||||
}
|
||||
}
|
||||
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
|
||||
return;
|
||||
|
||||
var ev = new SuffocationEvent();
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
}
|
||||
|
||||
private void StopSuffocation(Entity<RespiratorComponent> ent)
|
||||
@@ -372,6 +380,22 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
|
||||
|
||||
var ev = new StopSuffocatingEvent();
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
}
|
||||
|
||||
private void OnSuffocation(Entity<BodyComponent> ent, ref SuffocationEvent args)
|
||||
{
|
||||
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
foreach (var entity in organs)
|
||||
{
|
||||
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStopSuffocating(Entity<BodyComponent> ent, ref StopSuffocatingEvent args)
|
||||
{
|
||||
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
foreach (var entity in organs)
|
||||
@@ -397,6 +421,9 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
private void OnGasInhaled(Entity<BodyComponent> entity, ref InhaledGasEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
args.Succeeded = TryInhaleGasToBody((entity, entity.Comp), args.Gas);
|
||||
@@ -404,20 +431,65 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
private void OnGasExhaled(Entity<BodyComponent> entity, ref ExhaledGasEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
RemoveGasFromBody(entity, args.Gas);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity first tries to inhale that returns a GasMixture from a given location.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas that gets returned, null if there is none.</param>
|
||||
/// <param name="Respirator">The Respirator component of the entity attempting to inhale</param>
|
||||
[ByRefEvent]
|
||||
public record struct InhaleLocationEvent(GasMixture? Gas, RespiratorComponent Respirator);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity first tries to exhale a gas, determines where the gas they're exhaling will be sent.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas mixture that the exhaled gas will be merged into.</param>
|
||||
[ByRefEvent]
|
||||
public record struct ExhaleLocationEvent(GasMixture? Gas);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity successfully inhales a gas, attempts to find a place to put the gas.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas we're inhaling.</param>
|
||||
/// <param name="Handled">Whether a system has responded appropriately.</param>
|
||||
/// <param name="Succeeded">Whether we successfully managed to inhale the gas</param>
|
||||
[ByRefEvent]
|
||||
public record struct InhaledGasEvent(GasMixture Gas, bool Handled = false, bool Succeeded = false);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity is exhaling
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas mixture we're exhaling into.</param>
|
||||
/// <param name="Handled">Whether we have successfully exhaled or not.</param>
|
||||
[ByRefEvent]
|
||||
public record struct ExhaledGasEvent(GasMixture Gas, bool Handled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an entity starts suffocating and when suffocation progresses.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct SuffocationEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an entity that was suffocating stops suffocating.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct StopSuffocatingEvent;
|
||||
|
||||
/// <summary>
|
||||
/// An event raised to inhalation handlers that asks them nicely to simulate what it would be like to metabolize
|
||||
/// a given volume of gas, without actually metabolizing it.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas mixture we are testing.</param>
|
||||
/// <param name="Toxic">Whether the gas returns as toxic to any respirator.</param>
|
||||
/// <param name="Saturation">The amount of saturation we got from the gas.</param>
|
||||
[ByRefEvent]
|
||||
public record struct CanMetabolizeGasEvent(GasMixture Gas, bool Toxic = false, float Saturation = 0f, bool Handled = false);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Frozen;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -10,6 +12,8 @@ namespace Content.Server.Chat.Systems;
|
||||
// emotes using emote prototype
|
||||
public partial class ChatSystem
|
||||
{
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
|
||||
private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
|
||||
|
||||
protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj)
|
||||
@@ -51,7 +55,8 @@ public partial class ChatSystem
|
||||
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
|
||||
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
|
||||
public void TryEmoteWithChat(
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithChat(
|
||||
EntityUid source,
|
||||
string emoteId,
|
||||
ChatTransmitRange range = ChatTransmitRange.Normal,
|
||||
@@ -62,8 +67,8 @@ public partial class ChatSystem
|
||||
)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
|
||||
return;
|
||||
TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
|
||||
return false;
|
||||
return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,7 +81,8 @@ public partial class ChatSystem
|
||||
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
|
||||
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
|
||||
public void TryEmoteWithChat(
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithChat(
|
||||
EntityUid source,
|
||||
EmotePrototype emote,
|
||||
ChatTransmitRange range = ChatTransmitRange.Normal,
|
||||
@@ -84,43 +90,46 @@ public partial class ChatSystem
|
||||
string? nameOverride = null,
|
||||
bool ignoreActionBlocker = false,
|
||||
bool forceEmote = false
|
||||
)
|
||||
)
|
||||
{
|
||||
if (!forceEmote && !AllowedToUseEmote(source, emote))
|
||||
return;
|
||||
return false;
|
||||
|
||||
var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
|
||||
|
||||
// check if proto has valid message for chat
|
||||
if (emote.ChatMessages.Count != 0)
|
||||
if (didEmote && emote.ChatMessages.Count != 0)
|
||||
{
|
||||
// not all emotes are loc'd, but for the ones that are we pass in entity
|
||||
var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
|
||||
SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
|
||||
}
|
||||
|
||||
// do the rest of emote event logic here
|
||||
TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
|
||||
return didEmote;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||
/// </summary>
|
||||
public void TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
|
||||
return;
|
||||
return false;
|
||||
|
||||
TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
|
||||
return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||
/// </summary>
|
||||
public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
|
||||
{
|
||||
if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
|
||||
return;
|
||||
return false;
|
||||
|
||||
InvokeEmoteEvent(uid, proto);
|
||||
return TryInvokeEmoteEvent(uid, proto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,17 +169,17 @@ public partial class ChatSystem
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="textInput"></param>
|
||||
private void TryEmoteChatInput(EntityUid uid, string textInput)
|
||||
/// <returns>True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be.</returns>
|
||||
private bool TryEmoteChatInput(EntityUid uid, string textInput)
|
||||
{
|
||||
var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
|
||||
if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
|
||||
return;
|
||||
return true;
|
||||
|
||||
if (!AllowedToUseEmote(uid, emote))
|
||||
return;
|
||||
return true;
|
||||
|
||||
InvokeEmoteEvent(uid, emote);
|
||||
return;
|
||||
return TryInvokeEmoteEvent(uid, emote);
|
||||
|
||||
static string TrimPunctuation(string textInput)
|
||||
{
|
||||
@@ -220,11 +229,50 @@ public partial class ChatSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
|
||||
/// <summary>
|
||||
/// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
|
||||
/// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player
|
||||
/// explaining why the emote didn't happen.
|
||||
/// </summary>
|
||||
/// <param name="uid">The entity which is emoting</param>
|
||||
/// <param name="proto">The emote which is being performed</param>
|
||||
/// <returns>True if the emote was performed, false otherwise.</returns>
|
||||
private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
|
||||
{
|
||||
var beforeEv = new BeforeEmoteEvent(uid, proto);
|
||||
RaiseLocalEvent(uid, ref beforeEv);
|
||||
|
||||
if (beforeEv.Cancelled)
|
||||
{
|
||||
if (beforeEv.Blocker != null)
|
||||
{
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString(
|
||||
"chat-system-emote-cancelled-blocked",
|
||||
("emote", Loc.GetString(proto.Name).ToLower()),
|
||||
("blocker", beforeEv.Blocker.Value)
|
||||
),
|
||||
uid,
|
||||
uid
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString("chat-system-emote-cancelled-generic",
|
||||
("emote", Loc.GetString(proto.Name).ToLower())),
|
||||
uid,
|
||||
uid
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var ev = new EmoteEvent(proto);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,9 +281,8 @@ public partial class ChatSystem
|
||||
/// Use it to play sound, change sprite or something else.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public struct EmoteEvent
|
||||
public sealed class EmoteEvent : HandledEntityEventArgs
|
||||
{
|
||||
public bool Handled;
|
||||
public readonly EmotePrototype Emote;
|
||||
|
||||
public EmoteEvent(EmotePrototype emote)
|
||||
|
||||
@@ -5,9 +5,8 @@ using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Server.Speech.Prototypes;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Speech.Prototypes;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
@@ -536,7 +535,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
|
||||
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
|
||||
|
||||
if (data.Range <= WhisperClearRange)
|
||||
if (data.Range <= WhisperClearRange || data.Observer)
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
|
||||
//If listener is too far, they only hear fragments of the message
|
||||
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
|
||||
@@ -593,8 +592,10 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
("entity", ent),
|
||||
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
|
||||
|
||||
if (checkEmote)
|
||||
TryEmoteChatInput(source, action);
|
||||
if (checkEmote &&
|
||||
!TryEmoteChatInput(source, action))
|
||||
return;
|
||||
|
||||
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
|
||||
if (!hideLog)
|
||||
if (name != Name(source))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Server.Animals.Systems;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
@@ -19,19 +19,19 @@ public sealed partial class TransformableContainerComponent : Component
|
||||
/// It will revert to this when emptied.
|
||||
/// /// It defaults to the description of the parent entity unless overwritten.
|
||||
/// </summary>
|
||||
[DataField("initialDescription")]
|
||||
[DataField]
|
||||
public string? InitialDescription;
|
||||
|
||||
/// <summary>
|
||||
/// This stores whatever primary reagent is currently in the container.
|
||||
/// It is used to help determine if a transformation is needed on solution update.
|
||||
/// </summary>
|
||||
[DataField("currentReagent")]
|
||||
public ReagentPrototype? CurrentReagent;
|
||||
[DataField]
|
||||
public ProtoId<ReagentPrototype>? CurrentReagent;
|
||||
|
||||
/// <summary>
|
||||
/// This returns whether this container in a transformed or initial state.
|
||||
/// </summary>
|
||||
///
|
||||
[DataField("transformed")]
|
||||
[DataField]
|
||||
public bool Transformed;
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ public sealed class TransformableContainerSystem : EntitySystem
|
||||
//the biggest reagent in the solution decides the appearance
|
||||
var reagentId = solution.GetPrimaryReagentId();
|
||||
|
||||
//If biggest reagent didn't changed - don't change anything at all
|
||||
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent.ID == reagentId?.Prototype)
|
||||
//If biggest reagent didn't change - don't change anything at all
|
||||
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent == reagentId?.Prototype)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public sealed class TransformableContainerSystem : EntitySystem
|
||||
|
||||
private void OnRefreshNameModifiers(Entity<TransformableContainerComponent> entity, ref RefreshNameModifiersEvent args)
|
||||
{
|
||||
if (entity.Comp.CurrentReagent is { } currentReagent)
|
||||
if (_prototypeManager.TryIndex(entity.Comp.CurrentReagent, out var currentReagent))
|
||||
{
|
||||
args.AddModifier("transformable-container-component-glass", priority: -1, ("reagent", currentReagent.LocalizedName));
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public sealed class CluwneSystem : EntitySystem
|
||||
else if (_robustRandom.Prob(component.KnockChance))
|
||||
{
|
||||
_audio.PlayPvs(component.KnockSound, uid);
|
||||
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(component.ParalyzeTime), true);
|
||||
_stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(component.ParalyzeTime));
|
||||
_chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ namespace Content.Server.Communications
|
||||
{
|
||||
// All events that refresh the BUI
|
||||
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, ComponentInit>((uid, comp, _) => UpdateCommsConsoleInterface(uid, comp));
|
||||
SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent());
|
||||
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>(_ => OnGenericBroadcastEvent());
|
||||
|
||||
@@ -85,6 +84,7 @@ namespace Content.Server.Communications
|
||||
public void OnCommunicationsConsoleMapInit(EntityUid uid, CommunicationsConsoleComponent comp, MapInitEvent args)
|
||||
{
|
||||
comp.AnnouncementCooldownRemaining = comp.InitialDelay;
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Damage.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component shows entity damage severity when it is examined by player.
|
||||
/// This component shows entity damage severity when it is examined by player.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ExaminableDamageComponent : Component
|
||||
{
|
||||
[DataField("messages", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<ExaminableDamagePrototype>))]
|
||||
public string? MessagesProtoId;
|
||||
|
||||
public ExaminableDamagePrototype? MessagesProto;
|
||||
/// <summary>
|
||||
/// ID of the <see cref="LocalizedDatasetPrototype"/> containing messages to display a different damage levels.
|
||||
/// The first message will be used at 0 damage with the others equally distributed across the range from undamaged to fully damaged.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<LocalizedDatasetPrototype>? Messages;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Damage.Components;
|
||||
using Content.Server.Damage.Components;
|
||||
using Content.Server.Destructible;
|
||||
using Content.Server.Destructible.Thresholds.Triggers;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Rounding;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -12,59 +9,42 @@ namespace Content.Server.Damage.Systems;
|
||||
|
||||
public sealed class ExaminableDamageSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DestructibleSystem _destructible = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<ExaminableDamageComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<ExaminableDamageComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, ExaminableDamageComponent component, ComponentInit args)
|
||||
private void OnExamine(Entity<ExaminableDamageComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (component.MessagesProtoId == null)
|
||||
if (!_prototype.TryIndex(ent.Comp.Messages, out var proto) || proto.Values.Count == 0)
|
||||
return;
|
||||
component.MessagesProto = _prototype.Index<ExaminableDamagePrototype>(component.MessagesProtoId);
|
||||
|
||||
var percent = GetDamagePercent(ent);
|
||||
var level = ContentHelpers.RoundToNearestLevels(percent, 1, proto.Values.Count - 1);
|
||||
var msg = Loc.GetString(proto.Values[level]);
|
||||
args.PushMarkup(msg, -99);
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, ExaminableDamageComponent component, ExaminedEvent args)
|
||||
/// <summary>
|
||||
/// Returns a value between 0 and 1 representing how damaged the entity is,
|
||||
/// where 0 is undamaged and 1 is fully damaged.
|
||||
/// </summary>
|
||||
/// <returns>How damaged the entity is from 0 to 1</returns>
|
||||
private float GetDamagePercent(Entity<ExaminableDamageComponent> ent)
|
||||
{
|
||||
if (component.MessagesProto == null)
|
||||
return;
|
||||
|
||||
var messages = component.MessagesProto.Messages;
|
||||
if (messages.Length == 0)
|
||||
return;
|
||||
|
||||
var level = GetDamageLevel(uid, component);
|
||||
var msg = Loc.GetString(messages[level]);
|
||||
args.PushMarkup(msg,-99);
|
||||
}
|
||||
|
||||
private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,
|
||||
DamageableComponent? damageable = null, DestructibleComponent? destructible = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, ref damageable, ref destructible))
|
||||
return 0;
|
||||
|
||||
if (component.MessagesProto == null)
|
||||
return 0;
|
||||
|
||||
var maxLevels = component.MessagesProto.Messages.Length - 1;
|
||||
if (maxLevels <= 0)
|
||||
return 0;
|
||||
|
||||
var trigger = (DamageTrigger?) destructible.Thresholds
|
||||
.LastOrDefault(threshold => threshold.Trigger is DamageTrigger)?.Trigger;
|
||||
if (trigger == null)
|
||||
if (!TryComp<DamageableComponent>(ent, out var damageable))
|
||||
return 0;
|
||||
|
||||
var damage = damageable.TotalDamage;
|
||||
var damageThreshold = trigger.Damage;
|
||||
var fraction = damageThreshold == 0 ? 0f : (float) damage / damageThreshold;
|
||||
var damageThreshold = _destructible.DestroyedAt(ent);
|
||||
|
||||
var level = ContentHelpers.RoundToNearestLevels(fraction, 1, maxLevels);
|
||||
return level;
|
||||
if (damageThreshold == 0)
|
||||
return 0;
|
||||
|
||||
return (damage / damageThreshold).Float();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Devour;
|
||||
using Content.Shared.Devour.Components;
|
||||
using Content.Shared.Whitelist;
|
||||
|
||||
namespace Content.Server.Devour;
|
||||
|
||||
public sealed class DevourSystem : SharedDevourSystem
|
||||
{
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _entityWhitelistSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DevourerComponent, DevourDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<DevourerComponent, BeingGibbedEvent>(OnGibContents);
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, DevourerComponent component, DevourDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled)
|
||||
return;
|
||||
|
||||
var ichorInjection = new Solution(component.Chemical, component.HealRate);
|
||||
|
||||
// Grant ichor if the devoured thing meets the dragon's food preference
|
||||
if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPassOrNull(component.FoodPreferenceWhitelist, (EntityUid)args.Args.Target))
|
||||
{
|
||||
_bloodstreamSystem.TryAddToChemicals(uid, ichorInjection);
|
||||
}
|
||||
|
||||
// If the devoured thing meets the stomach whitelist criteria, add it to the stomach
|
||||
if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPass(component.StomachStorageWhitelist, (EntityUid)args.Args.Target))
|
||||
{
|
||||
ContainerSystem.Insert(args.Args.Target.Value, component.Stomach);
|
||||
}
|
||||
//TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow.
|
||||
//If it's not alive, it must be a structure.
|
||||
// Delete if the thing isn't in the stomach storage whitelist (or the stomach whitelist is null/empty)
|
||||
else if (args.Args.Target != null)
|
||||
{
|
||||
QueueDel(args.Args.Target.Value);
|
||||
}
|
||||
|
||||
_audioSystem.PlayPvs(component.SoundDevour, uid);
|
||||
}
|
||||
|
||||
private void OnGibContents(EntityUid uid, DevourerComponent component, ref BeingGibbedEvent args)
|
||||
{
|
||||
if (component.StomachStorageWhitelist == null)
|
||||
return;
|
||||
|
||||
// For some reason we have two different systems that should handle gibbing,
|
||||
// and for some another reason GibbingSystem, which should empty all containers, doesn't get involved in this process
|
||||
ContainerSystem.EmptyContainer(component.Stomach);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Server.StatusEffectNew;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Drowsiness;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.StatusEffectNew.Components;
|
||||
|
||||
@@ -397,7 +397,12 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
|
||||
var shouldStun = siemensCoefficient > 0.5f;
|
||||
|
||||
if (shouldStun)
|
||||
_stun.TryParalyze(uid, time * ParalyzeTimeMultiplier, refresh, statusEffects);
|
||||
{
|
||||
_ = refresh
|
||||
? _stun.TryUpdateParalyzeDuration(uid, time * ParalyzeTimeMultiplier)
|
||||
: _stun.TryAddParalyzeDuration(uid, time * ParalyzeTimeMultiplier);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Sparks here.
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using System.Threading;
|
||||
using Content.Shared.Ensnaring.Components;
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,11 @@ public sealed class ProjectileGrenadeSystem : EntitySystem
|
||||
var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
|
||||
var shootCount = 0;
|
||||
var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
|
||||
|
||||
// Just in case
|
||||
if (totalCount == 0)
|
||||
return;
|
||||
|
||||
var segmentAngle = 360 / totalCount;
|
||||
|
||||
while (TrySpawnContents(grenadeCoord, component, out var contentUid))
|
||||
|
||||
@@ -338,6 +338,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// Ensure we actually have the component
|
||||
EnsureComp<TileFrictionModifierComponent>(entity);
|
||||
|
||||
EnsureComp<SlipperyComponent>(entity, out var slipComp);
|
||||
|
||||
// This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
|
||||
// the sprite threshold for a puddle larger than 5 pixels.
|
||||
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
|
||||
@@ -356,17 +358,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
var launchMult = FixedPoint2.Zero;
|
||||
// A cumulative weighted amount of stun times from slippery reagents
|
||||
var stunTimer = TimeSpan.Zero;
|
||||
// A cumulative weighted amount of knockdown times from slippery reagents
|
||||
var knockdownTimer = TimeSpan.Zero;
|
||||
|
||||
// Check if the puddle is big enough to slip in to avoid doing unnecessary logic
|
||||
if (solution.Volume <= smallPuddleThreshold)
|
||||
{
|
||||
_stepTrigger.SetActive(entity, false, comp);
|
||||
_tile.SetModifier(entity, 1f);
|
||||
slipComp.SlipData.SlipFriction = 1f;
|
||||
slipComp.AffectsSliding = false;
|
||||
Dirty(entity, slipComp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<SlipperyComponent>(entity, out var slipComp))
|
||||
return;
|
||||
slipComp.AffectsSliding = true;
|
||||
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
{
|
||||
@@ -386,7 +392,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// Aggregate launch speed based on quantity
|
||||
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
|
||||
// Aggregate stun times based on quantity
|
||||
stunTimer += reagentProto.SlipData.ParalyzeTime * (float)quantity;
|
||||
stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
|
||||
knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
|
||||
|
||||
if (reagentProto.SlipData.SuperSlippery)
|
||||
superSlipperyUnits += quantity;
|
||||
@@ -404,8 +411,9 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
|
||||
if (slipperyUnits > 0)
|
||||
{
|
||||
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
|
||||
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
|
||||
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
|
||||
slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
|
||||
slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
|
||||
}
|
||||
|
||||
// Only make it super slippery if there is enough super slippery units for its own puddle
|
||||
|
||||
@@ -228,7 +228,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
continue;
|
||||
|
||||
_npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
|
||||
_stun.TryParalyze(uid, stunTime, true);
|
||||
_stun.TryUpdateParalyzeDuration(uid, stunTime);
|
||||
RemCompDeferred<RevolutionaryComponent>(uid);
|
||||
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
|
||||
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Events;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Content.Server.Ghost.Roles.UI;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
@@ -698,7 +697,7 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
RaiseLocalEvent(mob, spawnedEvent);
|
||||
|
||||
if (ghostRole.MakeSentient)
|
||||
MakeSentientCommand.MakeSentient(mob, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
_mindSystem.MakeSentient(mob, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
|
||||
EnsureComp<MindContainerComponent>(mob);
|
||||
|
||||
@@ -745,7 +744,7 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
}
|
||||
|
||||
if (ghostRole.MakeSentient)
|
||||
MakeSentientCommand.MakeSentient(uid, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
_mindSystem.MakeSentient(uid, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
|
||||
GhostRoleInternalCreateMindAndTransfer(args.Player, uid, uid, ghostRole);
|
||||
UnregisterGhostRole((uid, ghostRole));
|
||||
|
||||
@@ -10,55 +10,46 @@ using Robust.Shared.Network;
|
||||
namespace Content.Server.Info;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class ShowRulesCommand : IConsoleCommand
|
||||
public sealed class ShowRulesCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configuration = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
|
||||
public string Command => "showrules";
|
||||
public string Description => "Opens the rules popup for the specified player.";
|
||||
public string Help => "showrules <username> [seconds]";
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
public override string Command => "showrules";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
string target;
|
||||
float seconds;
|
||||
|
||||
switch (args.Length)
|
||||
if (args.Length is < 1 or > 2)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
target = args[0];
|
||||
seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
if (!float.TryParse(args[1], out seconds))
|
||||
{
|
||||
shell.WriteError($"{args[1]} is not a valid amount of seconds.\n{Help}");
|
||||
return;
|
||||
}
|
||||
|
||||
target = args[0];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
var seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
|
||||
|
||||
if (!_player.TryGetSessionByUsername(target, out var player))
|
||||
if (args.Length == 2 && !float.TryParse(args[1], out seconds))
|
||||
{
|
||||
shell.WriteError("Unable to find a player with that name.");
|
||||
return;
|
||||
shell.WriteError(Loc.GetString("cmd-showrules-invalid-seconds", ("seconds", args[1])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_player.TryGetSessionByUsername(args[0], out var player))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-target-player-does-not-exist"));
|
||||
return;
|
||||
}
|
||||
|
||||
var coreRules = _configuration.GetCVar(CCVars.RulesFile);
|
||||
var message = new SendRulesInformationMessage { PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true};
|
||||
var message = new SendRulesInformationMessage
|
||||
{ PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true };
|
||||
_net.ServerSendMessage(message, player.Channel);
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
return args.Length == 1
|
||||
? CompletionResult.FromOptions(CompletionHelper.SessionNames(players: _player))
|
||||
: CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
{
|
||||
if (instrument.InstrumentPlayer is {Valid: true} mob)
|
||||
{
|
||||
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true);
|
||||
_stuns.TryUpdateParalyzeDuration(mob, TimeSpan.FromSeconds(1));
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"),
|
||||
uid, mob, PopupType.LargeCaution);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Materials.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.Audio;
|
||||
|
||||
namespace Content.Server.Materials;
|
||||
@@ -13,6 +14,7 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
@@ -39,7 +41,16 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
|
||||
var matAmount = solution.Value.Comp.Solution.Contents
|
||||
.Where(r => ent.Comp.ExtractionReagents.Contains(r.Reagent.Prototype))
|
||||
.Sum(r => r.Quantity.Float());
|
||||
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, (int) matAmount);
|
||||
|
||||
var changed = (int)matAmount;
|
||||
|
||||
if (changed == 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("material-extractor-comp-wrongreagent", ("used", args.Used)), args.User, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, changed);
|
||||
|
||||
_audio.PlayPvs(ent.Comp.ExtractSound, ent);
|
||||
QueueDel(args.Used);
|
||||
|
||||
@@ -2,16 +2,15 @@ using Content.Server.Body.Systems;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -27,7 +26,7 @@ namespace Content.Server.Medical
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly StunSystem _stun = default!;
|
||||
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
|
||||
[Dependency] private readonly ThirstSystem _thirst = default!;
|
||||
[Dependency] private readonly ForensicsSystem _forensics = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||
@@ -57,8 +56,7 @@ namespace Content.Server.Medical
|
||||
// It fully empties the stomach, this amount from the chem stream is relatively small
|
||||
var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6;
|
||||
// Apply a bit of slowdown
|
||||
if (TryComp<StatusEffectsComponent>(uid, out var status))
|
||||
_stun.TrySlowdown(uid, TimeSpan.FromSeconds(solutionSize), true, 0.5f, 0.5f, status);
|
||||
_movementMod.TryUpdateMovementSpeedModDuration(uid, MovementModStatusSystem.VomitingSlowdown, TimeSpan.FromSeconds(solutionSize), 0.5f);
|
||||
|
||||
// TODO: Need decals
|
||||
var solution = new Solution();
|
||||
|
||||
@@ -1,63 +1,30 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Mind.Commands
|
||||
namespace Content.Server.Mind.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class MakeSentientCommand : LocalizedEntityCommands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class MakeSentientCommand : IConsoleCommand
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
|
||||
public override string Command => "makesentient";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "makesentient";
|
||||
public string Description => "Makes an entity sentient (able to be controlled by a player)";
|
||||
public string Help => "makesentient <entity id>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
if (args.Length != 1)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine("Wrong number of arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var entNet) || !_entManager.TryGetEntity(entNet, out var entId))
|
||||
{
|
||||
shell.WriteLine("Invalid argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.EntityExists(entId))
|
||||
{
|
||||
shell.WriteLine("Invalid entity specified!");
|
||||
return;
|
||||
}
|
||||
|
||||
MakeSentient(entId.Value, _entManager, true, true);
|
||||
shell.WriteLine(Loc.GetString("shell-need-exactly-one-argument"));
|
||||
return;
|
||||
}
|
||||
|
||||
public static void MakeSentient(EntityUid uid, IEntityManager entityManager, bool allowMovement = true, bool allowSpeech = true)
|
||||
if (!NetEntity.TryParse(args[0], out var entNet) || !EntityManager.TryGetEntity(entNet, out var entId) || !EntityManager.EntityExists(entId))
|
||||
{
|
||||
entityManager.EnsureComponent<MindContainerComponent>(uid);
|
||||
if (allowMovement)
|
||||
{
|
||||
entityManager.EnsureComponent<InputMoverComponent>(uid);
|
||||
entityManager.EnsureComponent<MobMoverComponent>(uid);
|
||||
entityManager.EnsureComponent<MovementSpeedModifierComponent>(uid);
|
||||
}
|
||||
|
||||
if (allowSpeech)
|
||||
{
|
||||
entityManager.EnsureComponent<SpeechComponent>(uid);
|
||||
entityManager.EnsureComponent<EmotingComponent>(uid);
|
||||
}
|
||||
|
||||
entityManager.EnsureComponent<ExaminerComponent>(uid);
|
||||
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
_mindSystem.MakeSentient(entId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mind;
|
||||
@@ -349,7 +348,7 @@ public sealed class MindSystem : SharedMindSystem
|
||||
return;
|
||||
}
|
||||
|
||||
MakeSentientCommand.MakeSentient(target, EntityManager);
|
||||
MakeSentient(target);
|
||||
TransferTo(mindId, target, ghostCheckOverride: true, mind: mind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public sealed partial class NPCCombatSystem
|
||||
return;
|
||||
}
|
||||
|
||||
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates);
|
||||
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates, comp.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ public sealed class NameIdentifierSystem : EntitySystem
|
||||
randomVal = set[^1];
|
||||
set.RemoveAt(set.Count - 1);
|
||||
|
||||
return proto.Prefix is not null
|
||||
? $"{proto.Prefix}-{randomVal}"
|
||||
return proto.Format is not null
|
||||
? Loc.GetString(proto.Format, ("number", randomVal))
|
||||
: $"{randomVal}";
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ public sealed class NameIdentifierSystem : EntitySystem
|
||||
ids.Remove(ent.Comp.Identifier))
|
||||
{
|
||||
id = ent.Comp.Identifier;
|
||||
uniqueName = group.Prefix is not null
|
||||
? $"{group.Prefix}-{id}"
|
||||
uniqueName = group.Format is not null
|
||||
? Loc.GetString(group.Format, ("number", id))
|
||||
: $"{id}";
|
||||
}
|
||||
else
|
||||
|
||||
@@ -63,7 +63,7 @@ public sealed class StunProviderSystem : SharedStunProviderSystem
|
||||
_audio.PlayPvs(comp.Sound, target);
|
||||
|
||||
_damageable.TryChangeDamage(target, comp.StunDamage, false, true, null, origin: uid);
|
||||
_stun.TryParalyze(target, comp.StunTime, refresh: false);
|
||||
_stun.TryAddParalyzeDuration(target, comp.StunTime);
|
||||
|
||||
// short cooldown to prevent instant stunlocking
|
||||
_useDelay.SetLength((uid, useDelay), comp.Cooldown, id: comp.DelayId);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user