Merge remote-tracking branch 'upstream/master' into up/drinks-yml
@@ -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
@@ -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());
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -388,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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
@@ -19,6 +21,7 @@ namespace Content.Server.Power.EntitySystems;
|
||||
/// </remarks>
|
||||
public sealed class BatteryInterfaceSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLog = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -43,12 +46,16 @@ public sealed class BatteryInterfaceSystem : EntitySystem
|
||||
{
|
||||
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
|
||||
netBattery.CanCharge = args.On;
|
||||
|
||||
_adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set input breaker to {args.On} on {ToPrettyString(ent):target}");
|
||||
}
|
||||
|
||||
private void HandleSetOutputBreaker(Entity<BatteryInterfaceComponent> ent, ref BatterySetOutputBreakerMessage args)
|
||||
{
|
||||
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
|
||||
netBattery.CanDischarge = args.On;
|
||||
|
||||
_adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set output breaker to {args.On} on {ToPrettyString(ent):target}");
|
||||
}
|
||||
|
||||
private void HandleSetChargeRate(Entity<BatteryInterfaceComponent> ent, ref BatterySetChargeRateMessage args)
|
||||
|
||||
27
Content.Shared/Administration/AdminCameraEuiState.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Content.Shared.Eui;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class AdminCameraEuiState(NetEntity? camera, string name, GameTick tick) : EuiStateBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The camera entity we will use for the window.
|
||||
/// </summary>
|
||||
public readonly NetEntity? Camera = camera;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the observed entity.
|
||||
/// </summary>
|
||||
public readonly string Name = name;
|
||||
|
||||
/// <summary>
|
||||
/// The current tick time, needed for cursed reasons.
|
||||
/// </summary>
|
||||
public readonly GameTick Tick = tick;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class AdminCameraFollowMessage : EuiMessageBase;
|
||||
@@ -6,9 +6,10 @@ namespace Content.Shared.Eye
|
||||
[FlagsFor(typeof(VisibilityMaskLayer))]
|
||||
public enum VisibilityFlags : int
|
||||
{
|
||||
None = 0,
|
||||
None = 0,
|
||||
Normal = 1 << 0,
|
||||
Ghost = 1 << 1,
|
||||
Subfloor = 1 << 2,
|
||||
Ghost = 1 << 1, // Observers and revenants.
|
||||
Subfloor = 1 << 2, // Pipes, disposal chutes, cables etc. while hidden under tiles. Can be revealed with a t-ray.
|
||||
Admin = 1 << 3, // Reserved for admins in stealth mode and admin tools.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,9 @@ public sealed class FollowerSystem : EntitySystem
|
||||
/// <param name="entity">The entity to be followed</param>
|
||||
public void StartFollowingEntity(EntityUid follower, EntityUid entity)
|
||||
{
|
||||
if (follower == entity || TerminatingOrDeleted(entity))
|
||||
return;
|
||||
|
||||
// No recursion for you
|
||||
var targetXform = Transform(entity);
|
||||
while (targetXform.ParentUid.IsValid())
|
||||
|
||||
38
Content.Shared/KillTome/KillTomeComponent.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.KillTome;
|
||||
|
||||
/// <summary>
|
||||
/// Paper with that component is KillTome.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class KillTomeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// if delay is not specified, it will use this default value
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public TimeSpan DefaultKillDelay = TimeSpan.FromSeconds(40);
|
||||
|
||||
/// <summary>
|
||||
/// Damage specifier that will be used to kill the target.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public DamageSpecifier Damage = new()
|
||||
{
|
||||
DamageDict = new Dictionary<string, FixedPoint2>
|
||||
{
|
||||
{ "Blunt", 200 }
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// to keep a track of already killed people so they won't be killed again
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<EntityUid> KilledEntities = [];
|
||||
}
|
||||
187
Content.Shared/KillTome/KillTomeSystem.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Paper;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.KillTome;
|
||||
|
||||
/// <summary>
|
||||
/// This handles KillTome functionality.
|
||||
/// </summary>
|
||||
|
||||
/// Kill Tome Rules:
|
||||
// 1. The humanoid whose name is written in this note shall die.
|
||||
// 2. If the name is shared by multiple humanoids, a random humanoid with that name will die.
|
||||
// 3. Each name shall be written on a new line.
|
||||
// 4. Names must be written in the format: "Name, Delay (in seconds)" (e.g., John Doe, 40).
|
||||
// 5. A humanoid can be killed by the same Kill Tome only once.
|
||||
public sealed class KillTomeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageSystem = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogs = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameModifierSystem = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<KillTomeComponent, PaperAfterWriteEvent>(OnPaperAfterWriteInteract);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
// Getting all the entities that are targeted by Kill Tome and checking if their kill time has passed.
|
||||
// If it has, we kill them and remove the KillTomeTargetComponent.
|
||||
var query = EntityQueryEnumerator<KillTomeTargetComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var targetComp))
|
||||
{
|
||||
if (_gameTiming.CurTime < targetComp.KillTime)
|
||||
continue;
|
||||
|
||||
// The component doesn't get removed fast enough and the update loop will run through it a few more times.
|
||||
// This check is here to ensure it will not spam popups or kill you several times over.
|
||||
if (targetComp.Dead)
|
||||
continue;
|
||||
|
||||
Kill(uid, targetComp);
|
||||
|
||||
_popupSystem.PopupPredicted(Loc.GetString("killtome-death"),
|
||||
Loc.GetString("killtome-death-others", ("target", uid)),
|
||||
uid,
|
||||
uid,
|
||||
PopupType.LargeCaution);
|
||||
|
||||
targetComp.Dead = true;
|
||||
|
||||
RemCompDeferred<KillTomeTargetComponent>(uid);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPaperAfterWriteInteract(Entity<KillTomeComponent> ent, ref PaperAfterWriteEvent args)
|
||||
{
|
||||
// if the entity is not a paper, we don't do anything
|
||||
if (!TryComp<PaperComponent>(ent.Owner, out var paper))
|
||||
return;
|
||||
|
||||
var content = paper.Content;
|
||||
|
||||
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var showPopup = false;
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
var parts = line.Split(',', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var name = parts[0].Trim();
|
||||
|
||||
var delay = ent.Comp.DefaultKillDelay;
|
||||
|
||||
if (parts.Length == 2 && Parse.TryInt32(parts[1].Trim(), out var parsedDelay) && parsedDelay > 0)
|
||||
delay = TimeSpan.FromSeconds(parsedDelay);
|
||||
|
||||
if (!CheckIfEligible(name, ent.Comp, out var uid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compiler will complain if we don't check for null here.
|
||||
if (uid is not { } realUid)
|
||||
continue;
|
||||
|
||||
showPopup = true;
|
||||
|
||||
EnsureComp<KillTomeTargetComponent>(realUid, out var targetComp);
|
||||
|
||||
targetComp.KillTime = _gameTiming.CurTime + delay;
|
||||
targetComp.Damage = ent.Comp.Damage;
|
||||
|
||||
Dirty(realUid, targetComp);
|
||||
|
||||
ent.Comp.KilledEntities.Add(realUid);
|
||||
|
||||
Dirty(ent);
|
||||
|
||||
_adminLogs.Add(LogType.Chat,
|
||||
LogImpact.High,
|
||||
$"{ToPrettyString(args.Actor)} has written {ToPrettyString(uid)}'s name in Kill Tome.");
|
||||
}
|
||||
|
||||
// If we have written at least one eligible name, we show the popup (So the player knows death note worked).
|
||||
if (showPopup)
|
||||
_popupSystem.PopupEntity(Loc.GetString("killtome-kill-success"), ent.Owner, args.Actor, PopupType.Large);
|
||||
}
|
||||
|
||||
// A person to be killed by KillTome must:
|
||||
// 1. be with the name
|
||||
// 2. have HumanoidAppearanceComponent (so it targets only humanoids, obv)
|
||||
// 3. not be already dead
|
||||
// 4. not be already killed by Kill Tome
|
||||
|
||||
// If all these conditions are met, we return true and the entityUid of the person to kill.
|
||||
private bool CheckIfEligible(string name, KillTomeComponent comp, [NotNullWhen(true)] out EntityUid? entityUid)
|
||||
{
|
||||
if (!TryFindEntityByName(name, out var uid) ||
|
||||
!TryComp<MobStateComponent>(uid, out var mob))
|
||||
{
|
||||
entityUid = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uid is not { } realUid)
|
||||
{
|
||||
entityUid = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (comp.KilledEntities.Contains(realUid))
|
||||
{
|
||||
entityUid = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mob.CurrentState == MobState.Dead)
|
||||
{
|
||||
entityUid = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
entityUid = uid;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryFindEntityByName(string name, [NotNullWhen(true)] out EntityUid? entityUid)
|
||||
{
|
||||
var query = EntityQueryEnumerator<HumanoidAppearanceComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out _))
|
||||
{
|
||||
if (!_nameModifierSystem.GetBaseName(uid).Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
entityUid = uid;
|
||||
return true;
|
||||
}
|
||||
|
||||
entityUid = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Kill(EntityUid uid, KillTomeTargetComponent comp)
|
||||
{
|
||||
_damageSystem.TryChangeDamage(uid, comp.Damage, true);
|
||||
}
|
||||
}
|
||||
41
Content.Shared/KillTome/KillTomeTargetComponent.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.KillTome;
|
||||
|
||||
/// <summary>
|
||||
/// Entity with this component is a Kill Tome target.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
public sealed partial class KillTomeTargetComponent : Component
|
||||
{
|
||||
///<summary>
|
||||
/// Damage that will be dealt to the target.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public DamageSpecifier Damage = new()
|
||||
{
|
||||
DamageDict = new Dictionary<string, FixedPoint2>
|
||||
{
|
||||
{ "Blunt", 200 }
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The time when the target is killed.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
|
||||
[AutoPausedField]
|
||||
public TimeSpan KillTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates this target has been killed by the killtome.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Dead;
|
||||
|
||||
// Disallows cheat clients from seeing who is about to die to the killtome.
|
||||
public override bool SendOnlyToOwner => true;
|
||||
}
|
||||
@@ -187,6 +187,7 @@ public sealed class PaperSystem : EntitySystem
|
||||
{
|
||||
var ev = new PaperWriteAttemptEvent(entity.Owner);
|
||||
RaiseLocalEvent(args.Actor, ref ev);
|
||||
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
@@ -211,6 +212,9 @@ public sealed class PaperSystem : EntitySystem
|
||||
|
||||
entity.Comp.Mode = PaperAction.Read;
|
||||
UpdateUserInterface(entity);
|
||||
|
||||
var writeAfterEv = new PaperAfterWriteEvent(args.Actor);
|
||||
RaiseLocalEvent(entity.Owner, ref writeAfterEv);
|
||||
}
|
||||
|
||||
private void OnRandomPaperContentMapInit(Entity<RandomPaperContentComponent> ent, ref MapInitEvent args)
|
||||
@@ -319,6 +323,14 @@ public record struct PaperWriteEvent(EntityUid User, EntityUid Paper);
|
||||
/// <summary>
|
||||
/// Cancellable event for attempting to write on a piece of paper.
|
||||
/// </summary>
|
||||
/// <param name="paper">The paper that the writing will take place on.</param>
|
||||
/// <param name="Paper">The paper that the writing will take place on.</param>
|
||||
[ByRefEvent]
|
||||
public record struct PaperWriteAttemptEvent(EntityUid Paper, string? FailReason = null, bool Cancelled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on paper after it was written on by someone.
|
||||
/// </summary>
|
||||
/// <param name="Actor">Entity that wrote something on the paper.</param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct PaperAfterWriteEvent(EntityUid Actor);
|
||||
|
||||
|
||||
@@ -1285,5 +1285,13 @@ Entries:
|
||||
id: 156
|
||||
time: '2025-07-24T15:13:29.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/39025
|
||||
- author: slarticodefast, beck-thompson
|
||||
changes:
|
||||
- message: Added the "Open Camera" admin verb and the "camera" console command which
|
||||
allow you to observe entities in separate window using a remote camera.
|
||||
type: Add
|
||||
id: 157
|
||||
time: '2025-07-25T16:53:01.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/36969
|
||||
Name: Admin
|
||||
Order: 2
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
Entries:
|
||||
- author: Entvari
|
||||
changes:
|
||||
- message: Added a new Wizard headset.
|
||||
type: Add
|
||||
- message: The Wizard now starts with the Wizard headset.
|
||||
type: Tweak
|
||||
id: 8295
|
||||
time: '2025-04-20T21:38:30.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35732
|
||||
- author: aada
|
||||
changes:
|
||||
- message: Filter categories added to the circuit imprinter.
|
||||
type: Add
|
||||
id: 8296
|
||||
time: '2025-04-20T22:27:49.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35713
|
||||
- author: ScarKy0
|
||||
changes:
|
||||
- message: Mail spawning has been reworked. The mail teleporter now has to be manually
|
||||
emptied to take all the mail out. (You can do it by alt-clicking it or using
|
||||
a verb)
|
||||
type: Tweak
|
||||
- message: The mail teleporter now has an internal capacity of 20. It will not spawn
|
||||
mail past that until emptied by someone.
|
||||
type: Add
|
||||
id: 8297
|
||||
time: '2025-04-21T09:32:18.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/36774
|
||||
- author: ScarKy0
|
||||
changes:
|
||||
- message: Default cargo split is now 50% instead of 75%. Lockboxes remain unchanged.
|
||||
@@ -3911,3 +3883,28 @@
|
||||
id: 8806
|
||||
time: '2025-07-24T11:24:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/39177
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: Potassium-water explosion damage scaling has been tweaked to be less
|
||||
potent.
|
||||
type: Tweak
|
||||
id: 8807
|
||||
time: '2025-07-25T16:04:27.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/37924
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: Chemistry foam and smoke reactions now trigger after regular mix reactions.
|
||||
type: Tweak
|
||||
- message: Foam, smoke and explosion reactions no longer transfer the reagents'
|
||||
energies to the container.
|
||||
type: Tweak
|
||||
id: 8808
|
||||
time: '2025-07-25T16:33:44.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/37915
|
||||
- author: Quantum-cross
|
||||
changes:
|
||||
- message: pinpointer will no longer trigger sufferers of OCD
|
||||
type: Fix
|
||||
id: 8809
|
||||
time: '2025-07-25T19:46:42.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/38657
|
||||
|
||||
@@ -8,6 +8,8 @@ admin-verbs-teleport-here = Teleport Here
|
||||
admin-verbs-freeze = Freeze
|
||||
admin-verbs-freeze-and-mute = Freeze And Mute
|
||||
admin-verbs-unfreeze = Unfreeze
|
||||
admin-verbs-camera = Open Camera
|
||||
admin-verbs-camera-description = Open a camera window that follows the selected entity.
|
||||
admin-verbs-erase = Erase
|
||||
admin-verbs-erase-description = Removes the player from the round and crew manifest and deletes their chat messages.
|
||||
Their items are dropped on the ground.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
cmd-camera-desc = Opens a remote camera window for an entity.
|
||||
cmd-camera-help = Usage: camera <entityUid or player>
|
||||
|
||||
cmd-camera-hint = <entityUid or player>
|
||||
cmd-camera-wrong-argument = Argument must be a valid netUid or a player name.
|
||||
@@ -0,0 +1,5 @@
|
||||
admin-camera-window-title = Observing { $name }
|
||||
admin-camera-window-title-placeholder = Observing
|
||||
admin-camera-window-follow = Follow
|
||||
admin-camera-window-pop-out = Pop out
|
||||
admin-camera-window-pop-in = Pop in
|
||||
@@ -22,3 +22,4 @@ player-panel-rejuvenate = Rejuvenate
|
||||
player-panel-false = False
|
||||
player-panel-true = True
|
||||
player-panel-follow = Follow
|
||||
player-panel-camera = Camera
|
||||
|
||||
11
Resources/Locale/en-US/killtome.ftl
Normal file
@@ -0,0 +1,11 @@
|
||||
killtome-rules =
|
||||
Kill Tome Rules:
|
||||
1. The humanoid whose name is written in this note shall die.
|
||||
2. If the name is shared by multiple humanoids, a random humanoid with that name will die.
|
||||
3. Each name shall be written on a new line.
|
||||
4. Names must be written in the format: "Name, Delay (in seconds)" (e.g., John Doe, 40).
|
||||
5. A humanoid can be killed by the same Kill Tome only once.
|
||||
|
||||
killtome-kill-success = The name is written. The countdown begins.
|
||||
killtome-death = You feel sudden pain in your chest!
|
||||
killtome-death-others = {CAPITALIZE($target)} grabs onto {POSS-ADJ($target)} chest and falls to the ground!
|
||||
14
Resources/Prototypes/Entities/Interface/admin_tools.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
# dummy entity for the admin camera EUI
|
||||
# this gets parented to the person being observed
|
||||
|
||||
- type: entity
|
||||
id: AdminCamera
|
||||
categories: [ HideSpawnMenu ]
|
||||
name: admin camera
|
||||
description: We are watching you.
|
||||
components:
|
||||
- type: Visibility
|
||||
layer: 8 # Don't network this to anyone so cheat clients can't see it. We are adding a PVS override to the user.
|
||||
- type: Eye # for the camera itself
|
||||
drawFov: false
|
||||
pvsScale: 0.8 # we don't need the full range
|
||||
24
Resources/Prototypes/Entities/Objects/Misc/killtome.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
- type: entity
|
||||
name: black tome
|
||||
parent: BasePaper
|
||||
id: KillTome
|
||||
suffix: KillTome, Admeme # To stay true to the lore, please never make this accessible outside of divine intervention (admeme).
|
||||
description: A worn black tome. It smells like old paper.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Misc/killtome.rsi
|
||||
state: icon
|
||||
- type: KillTome
|
||||
defaultKillDelay: 40
|
||||
damage:
|
||||
types:
|
||||
Blunt: 200
|
||||
- type: Paper
|
||||
content: killtome-rules
|
||||
- type: ActivatableUI
|
||||
key: enum.PaperUiKey.Key
|
||||
requiresComplex: false
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.PaperUiKey.Key:
|
||||
type: PaperBoundUserInterface
|
||||
@@ -104,6 +104,7 @@
|
||||
id: PotassiumExplosion
|
||||
impact: High
|
||||
priority: 20
|
||||
conserveEnergy: false
|
||||
reactants:
|
||||
Water:
|
||||
amount: 1
|
||||
@@ -112,15 +113,16 @@
|
||||
effects:
|
||||
- !type:ExplosionReactionEffect
|
||||
explosionType: Default
|
||||
maxIntensity: 100
|
||||
intensityPerUnit: 0.5 # 50+50 reagent for maximum explosion
|
||||
intensitySlope: 4
|
||||
intensityPerUnit: 0.25
|
||||
maxTotalIntensity: 100
|
||||
intensitySlope: 4
|
||||
maxIntensity: 7
|
||||
|
||||
- type: reaction
|
||||
id: Smoke
|
||||
priority: 10
|
||||
priority: -10
|
||||
impact: High
|
||||
conserveEnergy: false
|
||||
reactants:
|
||||
Phosphorus:
|
||||
amount: 1
|
||||
@@ -137,8 +139,9 @@
|
||||
|
||||
- type: reaction
|
||||
id: Foam
|
||||
priority: 10
|
||||
priority: -10
|
||||
impact: High
|
||||
conserveEnergy: false
|
||||
reactants:
|
||||
Fluorosurfactant:
|
||||
amount: 1
|
||||
@@ -154,7 +157,8 @@
|
||||
- type: reaction
|
||||
id: IronMetalFoam
|
||||
impact: High
|
||||
priority: 10
|
||||
priority: -10
|
||||
conserveEnergy: false
|
||||
reactants:
|
||||
Iron:
|
||||
amount: 3
|
||||
@@ -172,7 +176,8 @@
|
||||
- type: reaction
|
||||
id: AluminiumMetalFoam
|
||||
impact: High
|
||||
priority: 10
|
||||
priority: -10
|
||||
conserveEnergy: false
|
||||
reactants:
|
||||
Aluminium:
|
||||
amount: 3
|
||||
@@ -191,6 +196,7 @@
|
||||
id: UraniumEmpExplosion
|
||||
impact: High
|
||||
priority: 20
|
||||
conserveEnergy: false
|
||||
reactants:
|
||||
Iron:
|
||||
amount: 1
|
||||
@@ -209,6 +215,7 @@
|
||||
id: Flash
|
||||
impact: High
|
||||
priority: 20
|
||||
conserveEnergy: false
|
||||
reactants:
|
||||
Aluminium:
|
||||
amount: 1
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"y": 32
|
||||
},
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/59f2a4e10e5ba36033c9734ddebfbbdc6157472d, station and syndicate pinpointer resprited by Ubaser, inhands by TiniestShark (github)",
|
||||
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/59f2a4e10e5ba36033c9734ddebfbbdc6157472d, station and syndicate pinpointer resprited by Ubaser, inhands by TiniestShark (github). Modified by @whatston3 and @Quantum-cross (github) to center the sprite so arrow rotation is around the proper axis.",
|
||||
"states": [
|
||||
{
|
||||
"name": "pinonalert",
|
||||
|
||||
|
Before Width: | Height: | Size: 577 B After Width: | Height: | Size: 555 B |
|
Before Width: | Height: | Size: 196 B After Width: | Height: | Size: 195 B |
|
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 528 B |
|
Before Width: | Height: | Size: 166 B After Width: | Height: | Size: 169 B |
|
Before Width: | Height: | Size: 173 B After Width: | Height: | Size: 179 B |
|
Before Width: | Height: | Size: 152 B After Width: | Height: | Size: 157 B |
|
Before Width: | Height: | Size: 159 B After Width: | Height: | Size: 172 B |
|
Before Width: | Height: | Size: 230 B After Width: | Height: | Size: 164 B |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 523 B |
|
Before Width: | Height: | Size: 182 B After Width: | Height: | Size: 194 B |
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 323 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 379 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 367 B |
|
Before Width: | Height: | Size: 266 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 434 B |
BIN
Resources/Textures/Objects/Misc/killtome.rsi/icon.png
Normal file
|
After Width: | Height: | Size: 558 B |
14
Resources/Textures/Objects/Misc/killtome.rsi/meta.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "alexmactep",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "icon"
|
||||
}
|
||||
]
|
||||
}
|
||||