Merge branch 'don' of https://github.com/TytosB/space-station-14-loop into don
This commit is contained in:
@@ -85,10 +85,10 @@ internal sealed class AdminNameOverlay : Overlay
|
||||
|
||||
var currentOffset = Vector2.Zero;
|
||||
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
||||
currentOffset += lineoffset;
|
||||
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||
currentOffset += lineoffset;
|
||||
|
||||
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && playTime)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem;
|
||||
@@ -0,0 +1,32 @@
|
||||
<Control xmlns="https://spacestation14.io" xmlns:system="clr-namespace:System;assembly=System.Runtime">
|
||||
<BoxContainer Name="MainContainer"
|
||||
Orientation="Horizontal"
|
||||
SetWidth="250">
|
||||
<Button Name="MainButton"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
StyleClasses="ButtonSquare"
|
||||
Margin="-1 0 0 0">
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="-5 0 0 0">
|
||||
<Label Name="TaskLabel"
|
||||
StyleClasses="LabelSubText" />
|
||||
<Label Name="TaskForLabel"
|
||||
StyleClasses="LabelSubText"
|
||||
Margin="0 -5 0 0" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Button>
|
||||
<Button Name="DoneButton"
|
||||
VerticalExpand="True"
|
||||
Text="{Loc 'nano-task-ui-done'}">
|
||||
<Button.StyleClasses>
|
||||
<system:String>ButtonSmall</system:String>
|
||||
<system:String>OpenLeft</system:String>
|
||||
</Button.StyleClasses>
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,33 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Maths;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single control for a single NanoTask item
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NanoTaskItemControl : Control
|
||||
{
|
||||
public Action<int>? OnMainPressed;
|
||||
public Action<int>? OnDonePressed;
|
||||
|
||||
public NanoTaskItemControl(NanoTaskItemAndId item)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
TaskLabel.Text = item.Data.Description;
|
||||
TaskLabel.FontColorOverride = Color.White;
|
||||
TaskForLabel.Text = item.Data.TaskIsFor;
|
||||
|
||||
MainButton.OnPressed += _ => OnMainPressed?.Invoke(item.Id);
|
||||
DoneButton.OnPressed += _ => OnDonePressed?.Invoke(item.Id);
|
||||
|
||||
MainButton.Disabled = item.Data.IsTaskDone;
|
||||
DoneButton.Text = item.Data.IsTaskDone ? Loc.GetString("nano-task-ui-revert-done") : Loc.GetString("nano-task-ui-done");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc nano-task-ui-item-title}"
|
||||
MinSize="300 300">
|
||||
<PanelContainer StyleClasses="AngleRect">
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<!-- Task Description Input -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-task-ui-description-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="DescriptionInput"
|
||||
PlaceHolder="{Loc nano-task-ui-description-placeholder}" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Task Requester Input -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4">
|
||||
<Label Text="{Loc nano-task-ui-requester-label}"
|
||||
StyleClasses="LabelHeading" />
|
||||
<PanelContainer StyleClasses="ButtonSquare">
|
||||
<LineEdit Name="RequesterInput"
|
||||
PlaceHolder="{Loc nano-task-ui-requester-placeholder}" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Severity Buttons -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 8 0 0">
|
||||
<Button Name="LowButton"
|
||||
Text="{Loc nano-task-ui-priority-low}"
|
||||
StyleClasses="OpenRight"
|
||||
MinSize="60 0" />
|
||||
<Button Name="MediumButton"
|
||||
Text="{Loc nano-task-ui-priority-medium}"
|
||||
StyleClasses="ButtonSquare"
|
||||
MinSize="60 0" />
|
||||
<Button Name="HighButton"
|
||||
Text="{Loc nano-task-ui-priority-high}"
|
||||
StyleClasses="OpenLeft"
|
||||
MinSize="60 0" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Verb Buttons -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 0 0">
|
||||
<Button Name="CancelButton"
|
||||
Text="{Loc nano-task-ui-cancel}"
|
||||
StyleClasses="OpenRight"
|
||||
MinSize="60 0" />
|
||||
<Button Name="DeleteButton"
|
||||
Text="{Loc nano-task-ui-delete}"
|
||||
StyleClasses="ButtonSquare"
|
||||
MinSize="60 0" />
|
||||
<Button Name="PrintButton"
|
||||
Text="{Loc nano-task-ui-print}"
|
||||
StyleClasses="ButtonSquare"
|
||||
MinSize="60 0" />
|
||||
<Button Name="SaveButton"
|
||||
Text="{Loc nano-task-ui-save}"
|
||||
StyleClasses="OpenLeft"
|
||||
MinSize="60 0" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</DefaultWindow>
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Popup displayed to edit a NanoTask item
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NanoTaskItemPopup : DefaultWindow
|
||||
{
|
||||
private readonly ButtonGroup _priorityGroup = new();
|
||||
private int? _editingTaskId = null;
|
||||
|
||||
public Action<int, NanoTaskItem>? TaskSaved;
|
||||
public Action<int>? TaskDeleted;
|
||||
public Action<NanoTaskItem>? TaskCreated;
|
||||
public Action<NanoTaskItem>? TaskPrinted;
|
||||
|
||||
private NanoTaskItem MakeItem()
|
||||
{
|
||||
return new(
|
||||
description: DescriptionInput.Text,
|
||||
taskIsFor: RequesterInput.Text,
|
||||
isTaskDone: false,
|
||||
priority: _priorityGroup.Pressed switch {
|
||||
var item when item == LowButton => NanoTaskPriority.Low,
|
||||
var item when item == MediumButton => NanoTaskPriority.Medium,
|
||||
var item when item == HighButton => NanoTaskPriority.High,
|
||||
_ => NanoTaskPriority.Medium,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public NanoTaskItemPopup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
LowButton.Group = _priorityGroup;
|
||||
MediumButton.Group = _priorityGroup;
|
||||
HighButton.Group = _priorityGroup;
|
||||
|
||||
CancelButton.OnPressed += _ => Close();
|
||||
DeleteButton.OnPressed += _ =>
|
||||
{
|
||||
if (_editingTaskId is int id)
|
||||
{
|
||||
TaskDeleted?.Invoke(id);
|
||||
}
|
||||
};
|
||||
PrintButton.OnPressed += _ =>
|
||||
{
|
||||
TaskPrinted?.Invoke(MakeItem());
|
||||
};
|
||||
SaveButton.OnPressed += _ =>
|
||||
{
|
||||
if (_editingTaskId is int id)
|
||||
{
|
||||
TaskSaved?.Invoke(id, MakeItem());
|
||||
}
|
||||
else
|
||||
{
|
||||
TaskCreated?.Invoke(MakeItem());
|
||||
}
|
||||
};
|
||||
|
||||
DescriptionInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > NanoTaskItem.MaximumStringLength)
|
||||
DescriptionInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
|
||||
};
|
||||
RequesterInput.OnTextChanged += args =>
|
||||
{
|
||||
if (args.Text.Length > NanoTaskItem.MaximumStringLength)
|
||||
RequesterInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
|
||||
};
|
||||
}
|
||||
|
||||
public void SetEditingTaskId(int? id)
|
||||
{
|
||||
_editingTaskId = id;
|
||||
DeleteButton.Visible = id is not null;
|
||||
}
|
||||
|
||||
public void ResetInputs(NanoTaskItem? item)
|
||||
{
|
||||
if (item is NanoTaskItem task)
|
||||
{
|
||||
var button = task.Priority switch {
|
||||
NanoTaskPriority.High => HighButton,
|
||||
NanoTaskPriority.Medium => MediumButton,
|
||||
NanoTaskPriority.Low => LowButton,
|
||||
};
|
||||
button.Pressed = true;
|
||||
DescriptionInput.Text = task.Description;
|
||||
RequesterInput.Text = task.TaskIsFor;
|
||||
}
|
||||
else
|
||||
{
|
||||
MediumButton.Pressed = true;
|
||||
DescriptionInput.Text = "";
|
||||
RequesterInput.Text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs
Normal file
82
Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Fragments;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// UI fragment responsible for displaying NanoTask controls in a PDA and coordinating with the NanoTaskCartridgeSystem for state
|
||||
/// </summary>
|
||||
public sealed partial class NanoTaskUi : UIFragment
|
||||
{
|
||||
private NanoTaskUiFragment? _fragment;
|
||||
private NanoTaskItemPopup? _popup;
|
||||
|
||||
public override Control GetUIFragmentRoot()
|
||||
{
|
||||
return _fragment!;
|
||||
}
|
||||
|
||||
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
|
||||
{
|
||||
_fragment = new NanoTaskUiFragment();
|
||||
_popup = new NanoTaskItemPopup();
|
||||
_fragment.NewTask += () =>
|
||||
{
|
||||
_popup.ResetInputs(null);
|
||||
_popup.SetEditingTaskId(null);
|
||||
_popup.OpenCentered();
|
||||
};
|
||||
_fragment.OpenTask += id =>
|
||||
{
|
||||
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
|
||||
return;
|
||||
|
||||
_popup.ResetInputs(task.Data);
|
||||
_popup.SetEditingTaskId(task.Id);
|
||||
_popup.OpenCentered();
|
||||
};
|
||||
_fragment.ToggleTaskCompletion += id =>
|
||||
{
|
||||
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
|
||||
return;
|
||||
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
|
||||
description: task.Data.Description,
|
||||
taskIsFor: task.Data.TaskIsFor,
|
||||
isTaskDone: !task.Data.IsTaskDone,
|
||||
priority: task.Data.Priority
|
||||
))))));
|
||||
};
|
||||
_popup.TaskSaved += (id, data) =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
|
||||
_popup.Close();
|
||||
};
|
||||
_popup.TaskDeleted += id =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
|
||||
_popup.Close();
|
||||
};
|
||||
_popup.TaskCreated += data =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
|
||||
_popup.Close();
|
||||
};
|
||||
_popup.TaskPrinted += data =>
|
||||
{
|
||||
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
|
||||
};
|
||||
}
|
||||
|
||||
public override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
if (state is not NanoTaskUiState nanoTaskState)
|
||||
return;
|
||||
|
||||
_fragment?.UpdateState(nanoTaskState.Tasks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<cartridges:NanoTaskUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns="https://spacestation14.io" Margin="1 0 2 0">
|
||||
|
||||
<PanelContainer StyleClasses="BackgroundDark"></PanelContainer>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical" Margin="8" SeparationOverride="8">
|
||||
<!-- Heading for High Priority Items -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<PanelContainer SetWidth="7" Margin="0 0 8 0">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#e93d58"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<Label Name="HighPriority" StyleClasses="LabelHeading"/>
|
||||
</BoxContainer>
|
||||
<!-- Location for High Priority Items -->
|
||||
<GridContainer Name="HighContainer"
|
||||
HorizontalExpand="True"
|
||||
Access="Public"
|
||||
Columns="2" />
|
||||
|
||||
<!-- Heading for Medium Priority Items -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<PanelContainer SetWidth="7" Margin="0 0 8 0">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#ef973c"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<Label Name="MediumPriority" StyleClasses="LabelHeading"/>
|
||||
</BoxContainer>
|
||||
<!-- Location for Medium Priority Items -->
|
||||
<GridContainer Name="MediumContainer"
|
||||
HorizontalExpand="True"
|
||||
Access="Public"
|
||||
Columns="2" />
|
||||
|
||||
<!-- Location for Low Priority Items -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<PanelContainer SetWidth="7" Margin="0 0 8 0">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#3dd425"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<Label Name="LowPriority" StyleClasses="LabelHeading"/>
|
||||
</BoxContainer>
|
||||
<!-- Location for Low Priority Items -->
|
||||
<GridContainer Name="LowContainer"
|
||||
HorizontalExpand="True"
|
||||
Access="Public"
|
||||
Columns="2" />
|
||||
|
||||
<Button Name="NewTaskButton" Text="{Loc 'nano-task-ui-new-task'}" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</cartridges:NanoTaskUiFragment>
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Client.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Class displaying the main UI of NanoTask
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class NanoTaskUiFragment : BoxContainer
|
||||
{
|
||||
public Action<int>? OpenTask;
|
||||
public Action<int>? ToggleTaskCompletion;
|
||||
public Action? NewTask;
|
||||
public List<NanoTaskItemAndId> Tasks = new();
|
||||
|
||||
public NanoTaskUiFragment()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
HorizontalExpand = true;
|
||||
VerticalExpand = true;
|
||||
NewTaskButton.OnPressed += _ => NewTask?.Invoke();
|
||||
}
|
||||
public void UpdateState(List<NanoTaskItemAndId> tasks)
|
||||
{
|
||||
Tasks = tasks;
|
||||
HighContainer.RemoveAllChildren();
|
||||
MediumContainer.RemoveAllChildren();
|
||||
LowContainer.RemoveAllChildren();
|
||||
|
||||
HighPriority.Text = Loc.GetString("nano-task-ui-heading-high-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.High)));
|
||||
MediumPriority.Text = Loc.GetString("nano-task-ui-heading-medium-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Medium)));
|
||||
LowPriority.Text = Loc.GetString("nano-task-ui-heading-low-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Low)));
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
var container = task.Data.Priority switch {
|
||||
NanoTaskPriority.High => HighContainer,
|
||||
NanoTaskPriority.Medium => MediumContainer,
|
||||
NanoTaskPriority.Low => LowContainer,
|
||||
};
|
||||
var control = new NanoTaskItemControl(task);
|
||||
container.AddChild(control);
|
||||
control.OnMainPressed += id => OpenTask?.Invoke(id);
|
||||
control.OnDonePressed += id => ToggleTaskCompletion?.Invoke(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,29 +36,6 @@ internal sealed class ShowSubFloor : LocalizedCommands
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShowSubFloorForever : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
public const string CommandName = "showsubfloorforever";
|
||||
public override string Command => CommandName;
|
||||
|
||||
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_entitySystemManager.GetEntitySystem<SubFloorHideSystem>().ShowAll = true;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var components = entMan.EntityQuery<SubFloorHideComponent, SpriteComponent>(true);
|
||||
|
||||
foreach (var (_, sprite) in components)
|
||||
{
|
||||
sprite.DrawDepth = (int) DrawDepth.Overlays;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class NotifyCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
|
||||
@@ -24,7 +24,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
|
||||
{
|
||||
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
|
||||
_lightManager.Enabled = false;
|
||||
shell.ExecuteCommand("showsubfloorforever");
|
||||
shell.ExecuteCommand("showsubfloor");
|
||||
_entitySystemManager.GetEntitySystem<ActionsSystem>().LoadActionAssignments("/mapping_actions.yml", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CriminalRecords;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Client.AutoGenerated;
|
||||
@@ -32,7 +33,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
public readonly EntityUid Console;
|
||||
|
||||
[ValidatePrototypeId<DatasetPrototype>]
|
||||
[ValidatePrototypeId<LocalizedDatasetPrototype>]
|
||||
private const string ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
|
||||
|
||||
public Action<uint?>? OnKeySelected;
|
||||
@@ -333,8 +334,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
var field = "reason";
|
||||
var title = Loc.GetString("criminal-records-status-" + status.ToString().ToLower());
|
||||
var placeholders = _proto.Index<DatasetPrototype>(ReasonPlaceholders);
|
||||
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders.Values))); // just funny it doesn't actually get used
|
||||
var placeholders = _proto.Index<LocalizedDatasetPrototype>(ReasonPlaceholders);
|
||||
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders))); // just funny it doesn't actually get used
|
||||
var prompt = Loc.GetString("criminal-records-console-reason");
|
||||
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
|
||||
var entries = new List<QuickDialogEntry>() { entry };
|
||||
|
||||
@@ -26,6 +26,12 @@ namespace Content.Client.IconSmoothing
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("key")]
|
||||
public string? SmoothKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional keys to smooth with.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<string> AdditionalKeys = new();
|
||||
|
||||
/// <summary>
|
||||
/// Prepended to the RSI state.
|
||||
/// </summary>
|
||||
|
||||
@@ -376,7 +376,8 @@ namespace Content.Client.IconSmoothing
|
||||
while (candidates.MoveNext(out var entity))
|
||||
{
|
||||
if (smoothQuery.TryGetComponent(entity, out var other) &&
|
||||
other.SmoothKey == smooth.SmoothKey &&
|
||||
other.SmoothKey != null &&
|
||||
(other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) &&
|
||||
other.Enabled)
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -64,9 +64,15 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
|
||||
|
||||
args.Sprite.Visible = hasVisibleLayer || revealed;
|
||||
|
||||
// allows a t-ray to show wires/pipes above carpets/puddles
|
||||
if (scannerRevealed)
|
||||
if (ShowAll)
|
||||
{
|
||||
// Allows sandbox mode to make wires visible over other stuff.
|
||||
component.OriginalDrawDepth ??= args.Sprite.DrawDepth;
|
||||
args.Sprite.DrawDepth = (int)Shared.DrawDepth.DrawDepth.Overdoors;
|
||||
}
|
||||
else if (scannerRevealed)
|
||||
{
|
||||
// Allows a t-ray to show wires/pipes above carpets/puddles.
|
||||
if (component.OriginalDrawDepth is not null)
|
||||
return;
|
||||
component.OriginalDrawDepth = args.Sprite.DrawDepth;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.DeviceLinking.Systems;
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Content.Shared.Prototypes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.DeviceLinking;
|
||||
|
||||
public sealed class DeviceLinkingTest
|
||||
{
|
||||
private const string PortTesterProtoId = "DeviceLinkingSinkPortTester";
|
||||
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = $@"
|
||||
- type: entity
|
||||
id: {PortTesterProtoId}
|
||||
components:
|
||||
- type: DeviceLinkSource
|
||||
ports:
|
||||
- Output
|
||||
";
|
||||
|
||||
/// <summary>
|
||||
/// Spawns every entity that has a <see cref="DeviceLinkSinkComponent"/>
|
||||
/// and sends a signal to every port to make sure nothing causes an error.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task AllDeviceLinkSinksWorkTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var mapMan = server.ResolveDependency<IMapManager>();
|
||||
var mapSys = server.System<SharedMapSystem>();
|
||||
var deviceLinkSys = server.System<DeviceLinkSystem>();
|
||||
|
||||
var prototypes = server.ProtoMan.EnumeratePrototypes<EntityPrototype>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in prototypes)
|
||||
{
|
||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (!proto.TryGetComponent<DeviceLinkSinkComponent>(out var protoSinkComp, compFact))
|
||||
continue;
|
||||
|
||||
foreach (var port in protoSinkComp.Ports)
|
||||
{
|
||||
// Create a map for each entity/port combo so they can't interfere
|
||||
mapSys.CreateMap(out var mapId);
|
||||
var grid = mapMan.CreateGridEntity(mapId);
|
||||
mapSys.SetTile(grid.Owner, grid.Comp, Vector2i.Zero, new Tile(1));
|
||||
var coord = new EntityCoordinates(grid.Owner, 0, 0);
|
||||
|
||||
// Spawn the sink entity
|
||||
var sinkEnt = server.EntMan.SpawnEntity(proto.ID, coord);
|
||||
// Get the actual sink component, since the one we got from the prototype doesn't have its owner set up
|
||||
Assert.That(server.EntMan.TryGetComponent<DeviceLinkSinkComponent>(sinkEnt, out var sinkComp),
|
||||
$"Tester prototype does not have a DeviceLinkSourceComponent!");
|
||||
|
||||
// Spawn the tester
|
||||
var sourceEnt = server.EntMan.SpawnEntity(PortTesterProtoId, coord);
|
||||
Assert.That(server.EntMan.TryGetComponent<DeviceLinkSourceComponent>(sourceEnt, out var sourceComp),
|
||||
$"Tester prototype does not have a DeviceLinkSourceComponent!");
|
||||
|
||||
// Create a link from the tester's output to the target port on the sink
|
||||
deviceLinkSys.SaveLinks(null,
|
||||
sourceEnt,
|
||||
sinkEnt,
|
||||
[("Output", port.Id)],
|
||||
sourceComp,
|
||||
sinkComp);
|
||||
|
||||
// Send a signal to the port
|
||||
Assert.DoesNotThrow(() => { deviceLinkSys.InvokePort(sourceEnt, "Output", null, sourceComp); },
|
||||
$"Exception thrown while triggering port {port.Id} of sink device {proto.ID}");
|
||||
|
||||
mapSys.DeleteMap(mapId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -152,10 +152,14 @@ public sealed class NukeOpsTest
|
||||
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
|
||||
}
|
||||
|
||||
var ruleGridComps = entMan.AllComponents<RuleGridsComponent>();
|
||||
Assert.That(ruleGridComps, Has.Length.EqualTo(1),
|
||||
$"Unexpected RuleGrid(s) detected! {string.Join(',', ruleGridComps.Select(e => server.EntMan.ToPrettyString(e.Uid)))}");
|
||||
|
||||
// The game rule exists, and all the stations/shuttles/maps are properly initialized
|
||||
var rule = entMan.AllComponents<NukeopsRuleComponent>().Single();
|
||||
var ruleComp = rule.Component;
|
||||
var gridsRule = entMan.AllComponents<RuleGridsComponent>().Single().Component;
|
||||
var gridsRule = ruleGridComps.Single().Component;
|
||||
foreach (var grid in gridsRule.MapGrids)
|
||||
{
|
||||
Assert.That(entMan.EntityExists(grid));
|
||||
|
||||
@@ -19,16 +19,19 @@ public sealed class LocalizedDatasetPrototypeTest
|
||||
|
||||
var protos = protoMan.EnumeratePrototypes<LocalizedDatasetPrototype>().OrderBy(p => p.ID);
|
||||
|
||||
// Check each prototype
|
||||
foreach (var proto in protos)
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Check each value in the prototype
|
||||
foreach (var locId in proto.Values)
|
||||
// Check each prototype
|
||||
foreach (var proto in protos)
|
||||
{
|
||||
// Make sure the localization manager has a string for the LocId
|
||||
Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
|
||||
// Check each value in the prototype
|
||||
foreach (var locId in proto.Values)
|
||||
{
|
||||
// Make sure the localization manager has a string for the LocId
|
||||
Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
@@ -102,6 +102,12 @@ public sealed class StoreTests
|
||||
+ $"flag as 'true'. This marks the fact that cost modifier of discount is not applied properly!"
|
||||
);
|
||||
|
||||
// The storeComponent returns discounted items with conditions randomly, so we remove these to sanitize the data.
|
||||
foreach (var discountedItem in discountedListingItems)
|
||||
{
|
||||
discountedItem.Conditions = null;
|
||||
}
|
||||
|
||||
// Refund action requests re-generation of listing items so we will be re-acquiring items from component a lot of times.
|
||||
var itemIds = discountedListingItems.Select(x => x.ID);
|
||||
foreach (var itemId in itemIds)
|
||||
@@ -140,6 +146,9 @@ public sealed class StoreTests
|
||||
// get refreshed item after refund re-generated items
|
||||
discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId);
|
||||
|
||||
// The storeComponent can give a discounted item a condition at random, so we remove it to sanitize the data.
|
||||
discountedListingItem.Conditions = null;
|
||||
|
||||
var afterRefundBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype];
|
||||
Assert.That(afterRefundBalance.Value, Is.EqualTo(originalBalance.Value), "Expected refund to return all discounted cost value.");
|
||||
Assert.That(
|
||||
|
||||
205
Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs
Normal file
205
Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Server.VendingMachines;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.VendingMachines;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Vending;
|
||||
|
||||
public sealed class VendingInteractionTest : InteractionTest
|
||||
{
|
||||
private const string VendingMachineProtoId = "InteractionTestVendingMachine";
|
||||
|
||||
private const string VendedItemProtoId = "InteractionTestItem";
|
||||
|
||||
private const string RestockBoxProtoId = "InteractionTestRestockBox";
|
||||
|
||||
private const string RestockBoxOtherProtoId = "InteractionTestRestockBoxOther";
|
||||
|
||||
[TestPrototypes]
|
||||
private const string TestPrototypes = $@"
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: {VendedItemProtoId}
|
||||
name: {VendedItemProtoId}
|
||||
|
||||
- type: vendingMachineInventory
|
||||
id: InteractionTestVendingInventory
|
||||
startingInventory:
|
||||
{VendedItemProtoId}: 5
|
||||
|
||||
- type: vendingMachineInventory
|
||||
id: InteractionTestVendingInventoryOther
|
||||
startingInventory:
|
||||
{VendedItemProtoId}: 5
|
||||
|
||||
- type: entity
|
||||
parent: BaseVendingMachineRestock
|
||||
id: {RestockBoxProtoId}
|
||||
components:
|
||||
- type: VendingMachineRestock
|
||||
canRestock:
|
||||
- InteractionTestVendingInventory
|
||||
|
||||
- type: entity
|
||||
parent: BaseVendingMachineRestock
|
||||
id: {RestockBoxOtherProtoId}
|
||||
components:
|
||||
- type: VendingMachineRestock
|
||||
canRestock:
|
||||
- InteractionTestVendingInventoryOther
|
||||
|
||||
- type: entity
|
||||
id: {VendingMachineProtoId}
|
||||
parent: VendingMachine
|
||||
components:
|
||||
- type: VendingMachine
|
||||
pack: InteractionTestVendingInventory
|
||||
ejectDelay: 0 # no delay to speed up tests
|
||||
- type: Sprite
|
||||
sprite: error.rsi
|
||||
";
|
||||
|
||||
[Test]
|
||||
public async Task InteractUITest()
|
||||
{
|
||||
await SpawnTarget(VendingMachineProtoId);
|
||||
|
||||
// Should start with no BUI open
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI was open unexpectedly.");
|
||||
|
||||
// Unpowered vending machine does not open BUI
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI opened without power.");
|
||||
|
||||
// Power the vending machine
|
||||
var apc = await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
// Interacting with powered vending machine opens BUI
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
|
||||
|
||||
// Interacting with it again closes the BUI
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on interaction.");
|
||||
|
||||
// Reopen BUI for the next check
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to reopen.");
|
||||
|
||||
// Remove power
|
||||
await Delete(apc);
|
||||
await RunTicks(1);
|
||||
|
||||
// The BUI should close when power is lost
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on power loss.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DispenseItemTest()
|
||||
{
|
||||
await SpawnTarget(VendingMachineProtoId);
|
||||
var vendorEnt = SEntMan.GetEntity(Target.Value);
|
||||
|
||||
var vendingSystem = SEntMan.System<VendingMachineSystem>();
|
||||
var items = vendingSystem.GetAllInventory(vendorEnt);
|
||||
|
||||
// Verify initial item count
|
||||
Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
|
||||
Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
|
||||
|
||||
// Power the vending machine
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
// Open the BUI
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
|
||||
|
||||
// Request an item be dispensed
|
||||
var ev = new VendingMachineEjectMessage(InventoryType.Regular, VendedItemProtoId);
|
||||
await SendBui(VendingMachineUiKey.Key, ev);
|
||||
|
||||
// Make sure the stock decreased
|
||||
Assert.That(items.First().Amount, Is.EqualTo(4), "Stocked item count did not decrease.");
|
||||
// Make sure the dispensed item was spawned in to the world
|
||||
await AssertEntityLookup(
|
||||
("APCBasic", 1),
|
||||
(VendedItemProtoId, 1)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RestockTest()
|
||||
{
|
||||
var vendingSystem = SEntMan.System<VendingMachineSystem>();
|
||||
|
||||
await SpawnTarget(VendingMachineProtoId);
|
||||
var vendorEnt = SEntMan.GetEntity(Target.Value);
|
||||
|
||||
var items = vendingSystem.GetAllInventory(vendorEnt);
|
||||
|
||||
Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
|
||||
Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
|
||||
|
||||
// Try to restock with the maintenance panel closed (nothing happens)
|
||||
await InteractUsing(RestockBoxProtoId);
|
||||
|
||||
Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked without opening maintenance panel.");
|
||||
|
||||
// Open the maintenance panel
|
||||
await InteractUsing(Screw);
|
||||
|
||||
// Try to restock using the wrong restock box (nothing happens)
|
||||
await InteractUsing(RestockBoxOtherProtoId);
|
||||
|
||||
Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked with wrong restock box.");
|
||||
|
||||
// Restock the machine
|
||||
await InteractUsing(RestockBoxProtoId);
|
||||
|
||||
Assert.That(items.First().Amount, Is.EqualTo(10), "Restocking resulted in unexpected item count.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RepairTest()
|
||||
{
|
||||
await SpawnTarget(VendingMachineProtoId);
|
||||
|
||||
// Power the vending machine
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
// Break it
|
||||
await BreakVendor();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI did not close when vending machine broke.");
|
||||
|
||||
// Make sure we can't open the BUI while it's broken
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "Opened BUI of broken vending machine.");
|
||||
|
||||
// Repair the vending machine
|
||||
await InteractUsing(Weld);
|
||||
|
||||
// Make sure the BUI can open now that the machine has been repaired
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(VendingMachineUiKey.Key), "Failed to open BUI after repair.");
|
||||
}
|
||||
|
||||
private async Task BreakVendor()
|
||||
{
|
||||
var damageableSys = SEntMan.System<DamageableSystem>();
|
||||
Assert.That(TryComp<DamageableComponent>(out var damageableComp), $"{VendingMachineProtoId} does not have DamageableComponent.");
|
||||
Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero), $"{VendingMachineProtoId} started with unexpected damage.");
|
||||
|
||||
// Damage the vending machine to the point that it breaks
|
||||
var damageType = ProtoMan.Index<DamageTypePrototype>("Blunt");
|
||||
var damage = new DamageSpecifier(damageType, FixedPoint2.New(100));
|
||||
await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
|
||||
await RunTicks(5);
|
||||
Assert.That(damageableComp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage.");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ using Content.Shared.Database;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Ban)]
|
||||
@@ -15,6 +17,7 @@ public sealed class RoleBanCommand : IConsoleCommand
|
||||
[Dependency] private readonly IPlayerLocator _locator = default!;
|
||||
[Dependency] private readonly IBanManager _bans = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
|
||||
public string Command => "roleban";
|
||||
public string Description => Loc.GetString("cmd-roleban-desc");
|
||||
@@ -76,6 +79,12 @@ public sealed class RoleBanCommand : IConsoleCommand
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_proto.HasIndex<JobPrototype>(job))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-job-parse",("job", job)));
|
||||
return;
|
||||
}
|
||||
|
||||
var located = await _locator.LookupIdByNameOrIdAsync(target);
|
||||
if (located == null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
@@ -14,9 +15,20 @@ public sealed class AntagRandomSpawnSystem : GameRuleSystem<AntagRandomSpawnComp
|
||||
SubscribeLocalEvent<AntagRandomSpawnComponent, AntagSelectLocationEvent>(OnSelectLocation);
|
||||
}
|
||||
|
||||
protected override void Added(EntityUid uid, AntagRandomSpawnComponent comp, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
{
|
||||
base.Added(uid, comp, gameRule, args);
|
||||
|
||||
// we have to select this here because AntagSelectLocationEvent is raised twice because MakeAntag is called twice
|
||||
// once when a ghost role spawner is created and once when someone takes the ghost role
|
||||
|
||||
if (TryFindRandomTile(out _, out _, out _, out var coords))
|
||||
comp.Coords = coords;
|
||||
}
|
||||
|
||||
private void OnSelectLocation(Entity<AntagRandomSpawnComponent> ent, ref AntagSelectLocationEvent args)
|
||||
{
|
||||
if (TryFindRandomTile(out _, out _, out _, out var coords))
|
||||
args.Coordinates.Add(_transform.ToMapCoordinates(coords));
|
||||
if (ent.Comp.Coords != null)
|
||||
args.Coordinates.Add(_transform.ToMapCoordinates(ent.Comp.Coords.Value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ using System.Linq;
|
||||
using Content.Server.Antag.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Preferences;
|
||||
using JetBrains.Annotations;
|
||||
@@ -25,7 +27,7 @@ public sealed partial class AntagSelectionSystem
|
||||
definition = null;
|
||||
|
||||
var totalTargetCount = GetTargetAntagCount(ent, players);
|
||||
var mindCount = ent.Comp.SelectedMinds.Count;
|
||||
var mindCount = ent.Comp.AssignedMinds.Count;
|
||||
if (mindCount >= totalTargetCount)
|
||||
return false;
|
||||
|
||||
@@ -95,7 +97,7 @@ public sealed partial class AntagSelectionSystem
|
||||
var countOffset = 0;
|
||||
foreach (var otherDef in ent.Comp.Definitions)
|
||||
{
|
||||
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
|
||||
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio; // Note: Is the PlayerRatio necessary here? Seems like it can cause issues for defs with varied PlayerRatio.
|
||||
}
|
||||
// make sure we don't double-count the current selection
|
||||
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
|
||||
@@ -115,7 +117,7 @@ public sealed partial class AntagSelectionSystem
|
||||
return new List<(EntityUid, SessionData, string)>();
|
||||
|
||||
var output = new List<(EntityUid, SessionData, string)>();
|
||||
foreach (var (mind, name) in ent.Comp.SelectedMinds)
|
||||
foreach (var (mind, name) in ent.Comp.AssignedMinds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
@@ -137,7 +139,7 @@ public sealed partial class AntagSelectionSystem
|
||||
return new();
|
||||
|
||||
var output = new List<Entity<MindComponent>>();
|
||||
foreach (var (mind, _) in ent.Comp.SelectedMinds)
|
||||
foreach (var (mind, _) in ent.Comp.AssignedMinds)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||
continue;
|
||||
@@ -155,7 +157,7 @@ public sealed partial class AntagSelectionSystem
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return new();
|
||||
|
||||
return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
|
||||
return ent.Comp.AssignedMinds.Select(p => p.Item1).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -247,7 +249,7 @@ public sealed partial class AntagSelectionSystem
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
|
||||
return GetAliveAntagCount(ent) == ent.Comp.AssignedMinds.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -352,8 +354,66 @@ public sealed partial class AntagSelectionSystem
|
||||
var ruleEnt = GameTicker.AddGameRule(id);
|
||||
RemComp<LoadMapRuleComponent>(ruleEnt);
|
||||
var antag = Comp<AntagSelectionComponent>(ruleEnt);
|
||||
antag.SelectionsComplete = true; // don't do normal selection.
|
||||
antag.AssignmentComplete = true; // don't do normal selection.
|
||||
GameTicker.StartGameRule(ruleEnt);
|
||||
return (ruleEnt, antag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all sessions that have been preselected for antag.
|
||||
/// </summary>
|
||||
/// <param name="except">A specific definition to be excluded from the check.</param>
|
||||
public HashSet<ICommonSession> GetPreSelectedAntagSessions(AntagSelectionDefinition? except = null)
|
||||
{
|
||||
var result = new HashSet<ICommonSession>();
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var def in comp.Definitions)
|
||||
{
|
||||
if (def.Equals(except))
|
||||
continue;
|
||||
|
||||
if (comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
result.UnionWith(set);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all sessions that have been preselected for antag and are exclusive, i.e. should not be paired with other antags.
|
||||
/// </summary>
|
||||
/// <param name="except">A specific definition to be excluded from the check.</param>
|
||||
// Note: This is a bit iffy since technically this exclusive definition is defined via the MultiAntagSetting, while there's a separately tracked antagExclusive variable in the mindrole.
|
||||
// We can't query that however since there's no guarantee the mindrole has been given out yet when checking pre-selected antags.
|
||||
// I don't think there's any instance where they differ, but it's something to be aware of for a potential future refactor.
|
||||
public HashSet<ICommonSession> GetPreSelectedExclusiveAntagSessions(AntagSelectionDefinition? except = null)
|
||||
{
|
||||
var result = new HashSet<ICommonSession>();
|
||||
var query = QueryAllRules();
|
||||
while (query.MoveNext(out var uid, out var comp, out _))
|
||||
{
|
||||
if (HasComp<EndedGameRuleComponent>(uid))
|
||||
continue;
|
||||
|
||||
foreach (var def in comp.Definitions)
|
||||
{
|
||||
if (def.Equals(except))
|
||||
continue;
|
||||
|
||||
if (def.MultiAntagSetting == AntagAcceptability.None && comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
{
|
||||
result.UnionWith(set);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Station.Events;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Ghost;
|
||||
@@ -46,6 +49,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
[Dependency] private readonly RoleSystem _role = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
|
||||
// arbitrary random number to give late joining some mild interest.
|
||||
public const float LateJoinRandomChance = 0.5f;
|
||||
@@ -89,19 +93,33 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||
{
|
||||
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
|
||||
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
|
||||
continue;
|
||||
|
||||
if (comp.SelectionsComplete)
|
||||
if (comp.AssignmentComplete)
|
||||
continue;
|
||||
|
||||
ChooseAntags((uid, comp), pool); // We choose the antags here...
|
||||
|
||||
if (comp.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
|
||||
{
|
||||
AssignPreSelectedSessions((uid, comp)); // ...But only assign them if PrePlayerSpawn
|
||||
foreach (var session in comp.AssignedSessions)
|
||||
{
|
||||
args.PlayerPool.Remove(session);
|
||||
GameTicker.PlayerJoinGame(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If IntraPlayerSpawn is selected, delayed rules should choose at this point too.
|
||||
var queryDelayed = QueryDelayedRules();
|
||||
while (queryDelayed.MoveNext(out var uid, out _, out var comp, out _))
|
||||
{
|
||||
if (comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
|
||||
continue;
|
||||
|
||||
ChooseAntags((uid, comp), pool);
|
||||
|
||||
foreach (var session in comp.SelectedSessions)
|
||||
{
|
||||
args.PlayerPool.Remove(session);
|
||||
GameTicker.PlayerJoinGame(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +128,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
var query = QueryActiveRules();
|
||||
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||
{
|
||||
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
|
||||
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
|
||||
continue;
|
||||
|
||||
ChooseAntags((uid, comp), args.Players);
|
||||
AssignPreSelectedSessions((uid, comp));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +145,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
// eventually this should probably store the players per definition with some kind of unique identifier.
|
||||
// something to figure out later.
|
||||
|
||||
var query = QueryActiveRules();
|
||||
var query = QueryAllRules();
|
||||
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
||||
while (query.MoveNext(out var uid, out _, out var antag, out _))
|
||||
while (query.MoveNext(out var uid, out var antag, out _))
|
||||
{
|
||||
rules.Add((uid, antag));
|
||||
if (HasComp<ActiveGameRuleComponent>(uid) ||
|
||||
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
|
||||
rules.Add((uid, antag));
|
||||
}
|
||||
RobustRandom.Shuffle(rules);
|
||||
|
||||
@@ -142,7 +163,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
|
||||
continue;
|
||||
|
||||
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
|
||||
DebugTools.AssertNotEqual(antag.SelectionTime, AntagSelectionTime.PrePlayerSpawn);
|
||||
|
||||
// do not count players in the lobby for the antag ratio
|
||||
var players = _playerManager.NetworkedSessions.Count(x => x.AttachedEntity != null);
|
||||
@@ -150,7 +171,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
|
||||
continue;
|
||||
|
||||
if (TryMakeAntag((uid, antag), args.Player, def.Value))
|
||||
var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet
|
||||
|
||||
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -183,14 +206,20 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (GameTicker.RunLevel != GameRunLevel.InRound)
|
||||
return;
|
||||
|
||||
if (component.SelectionsComplete)
|
||||
if (component.AssignmentComplete)
|
||||
return;
|
||||
|
||||
var players = _playerManager.Sessions
|
||||
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
|
||||
.ToList();
|
||||
if (!component.PreSelectionsComplete)
|
||||
{
|
||||
var players = _playerManager.Sessions
|
||||
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
|
||||
status == PlayerGameStatus.JoinedGame)
|
||||
.ToList();
|
||||
|
||||
ChooseAntags((uid, component), players, midround: true);
|
||||
ChooseAntags((uid, component), players, midround: true);
|
||||
}
|
||||
|
||||
AssignPreSelectedSessions((uid, component));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -201,7 +230,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
|
||||
{
|
||||
if (ent.Comp.SelectionsComplete)
|
||||
if (ent.Comp.PreSelectionsComplete)
|
||||
return;
|
||||
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
@@ -209,7 +238,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
ChooseAntags(ent, pool, def, midround: midround);
|
||||
}
|
||||
|
||||
ent.Comp.SelectionsComplete = true;
|
||||
ent.Comp.PreSelectionsComplete = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -250,21 +279,53 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
break;
|
||||
}
|
||||
|
||||
if (session != null && ent.Comp.SelectedSessions.Contains(session))
|
||||
if (session != null && ent.Comp.PreSelectedSessions.Values.Any(x => x.Contains(session)))
|
||||
{
|
||||
Log.Warning($"Somehow picked {session} for an antag when this rule already selected them previously");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
MakeAntag(ent, session, def);
|
||||
if (session == null)
|
||||
MakeAntag(ent, null, def); // This is for spawner antags
|
||||
else
|
||||
{
|
||||
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
|
||||
set.Add(session); // Selection done!
|
||||
Log.Debug($"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
|
||||
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns antag roles to sessions selected for it.
|
||||
/// </summary>
|
||||
public void AssignPreSelectedSessions(Entity<AntagSelectionComponent> ent)
|
||||
{
|
||||
// Only assign if there's been a pre-selection, and the selection hasn't already been made
|
||||
if (!ent.Comp.PreSelectionsComplete || ent.Comp.AssignmentComplete)
|
||||
return;
|
||||
|
||||
foreach (var def in ent.Comp.Definitions)
|
||||
{
|
||||
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
continue;
|
||||
|
||||
foreach (var session in set)
|
||||
{
|
||||
TryMakeAntag(ent, session, def);
|
||||
}
|
||||
}
|
||||
|
||||
ent.Comp.AssignmentComplete = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to makes a given player into the specified antagonist.
|
||||
/// </summary>
|
||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true)
|
||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
|
||||
{
|
||||
if (checkPref && !HasPrimaryAntagPreference(session, def))
|
||||
return false;
|
||||
@@ -272,7 +333,19 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
|
||||
return false;
|
||||
|
||||
MakeAntag(ent, session, def, ignoreSpawner);
|
||||
if (onlyPreSelect && session != null)
|
||||
{
|
||||
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
|
||||
set.Add(session);
|
||||
Log.Debug($"Pre-selected {session!.Name} as antagonist: {ToPrettyString(ent)}");
|
||||
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
MakeAntag(ent, session, def, ignoreSpawner);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -286,7 +359,10 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
|
||||
if (session != null)
|
||||
{
|
||||
ent.Comp.SelectedSessions.Add(session);
|
||||
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
|
||||
set.Add(session);
|
||||
ent.Comp.AssignedSessions.Add(session);
|
||||
|
||||
// we shouldn't be blocking the entity if they're just a ghost or smth.
|
||||
if (!HasComp<GhostComponent>(session.AttachedEntity))
|
||||
@@ -309,10 +385,19 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
{
|
||||
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
|
||||
if (session != null)
|
||||
ent.Comp.SelectedSessions.Remove(session);
|
||||
{
|
||||
ent.Comp.AssignedSessions.Remove(session);
|
||||
ent.Comp.PreSelectedSessions[def].Remove(session);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This is really messy because this part runs twice for midround events.
|
||||
// Once when the ghostrole spawner is created and once when a player takes it.
|
||||
// Therefore any component subscribing to this has to make sure both subscriptions return the same value
|
||||
// or the ghost role raffle location preview will be wrong.
|
||||
|
||||
var getPosEv = new AntagSelectLocationEvent(session, ent);
|
||||
RaiseLocalEvent(ent, ref getPosEv, true);
|
||||
if (getPosEv.Handled)
|
||||
@@ -330,7 +415,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
{
|
||||
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
|
||||
if (session != null)
|
||||
ent.Comp.SelectedSessions.Remove(session);
|
||||
{
|
||||
ent.Comp.AssignedSessions.Remove(session);
|
||||
ent.Comp.PreSelectedSessions[def].Remove(session);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -363,10 +452,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
|
||||
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
|
||||
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
|
||||
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
|
||||
ent.Comp.AssignedMinds.Add((curMind.Value, Name(player)));
|
||||
SendBriefing(session, def.Briefing);
|
||||
|
||||
Log.Debug($"Selected {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
|
||||
Log.Debug($"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
|
||||
_adminLogger.Add(LogType.AntagSelection, $"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
|
||||
}
|
||||
|
||||
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
|
||||
@@ -412,15 +502,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
|
||||
return false;
|
||||
|
||||
if (ent.Comp.SelectedSessions.Contains(session))
|
||||
if (ent.Comp.AssignedSessions.Contains(session))
|
||||
return false;
|
||||
|
||||
mind ??= session.GetMind();
|
||||
|
||||
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
|
||||
if (mind == null)
|
||||
return true;
|
||||
|
||||
//todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
|
||||
|
||||
switch (def.MultiAntagSetting)
|
||||
@@ -429,12 +515,16 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
{
|
||||
if (_role.MindIsAntagonist(mind))
|
||||
return false;
|
||||
if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case AntagAcceptability.NotExclusive:
|
||||
{
|
||||
if (_role.MindIsExclusiveAntagonist(mind))
|
||||
return false;
|
||||
if (GetPreSelectedExclusiveAntagSessions(def).Contains(session))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -481,7 +571,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (ent.Comp.AgentName is not { } name)
|
||||
return;
|
||||
|
||||
args.Minds = ent.Comp.SelectedMinds;
|
||||
args.Minds = ent.Comp.AssignedMinds;
|
||||
args.AgentName = Loc.GetString(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,4 +7,11 @@ namespace Content.Server.Antag.Components;
|
||||
/// Requires <see cref="AntagSelectionComponent"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class AntagRandomSpawnComponent : Component;
|
||||
public sealed partial class AntagRandomSpawnComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Location that was picked.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityCoordinates? Coords;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,16 @@ namespace Content.Server.Antag.Components;
|
||||
public sealed partial class AntagSelectionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Has the primary selection of antagonists finished yet?
|
||||
/// Has the primary assignment of antagonists finished yet?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool SelectionsComplete;
|
||||
public bool AssignmentComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Has the antagonists been preselected but yet to be fully assigned?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool PreSelectionsComplete;
|
||||
|
||||
/// <summary>
|
||||
/// The definitions for the antagonists
|
||||
@@ -26,10 +32,10 @@ public sealed partial class AntagSelectionComponent : Component
|
||||
public List<AntagSelectionDefinition> Definitions = new();
|
||||
|
||||
/// <summary>
|
||||
/// The minds and original names of the players selected to be antagonists.
|
||||
/// The minds and original names of the players assigned to be antagonists.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<(EntityUid, string)> SelectedMinds = new();
|
||||
public List<(EntityUid, string)> AssignedMinds = new();
|
||||
|
||||
/// <summary>
|
||||
/// When the antag selection will occur.
|
||||
@@ -37,11 +43,17 @@ public sealed partial class AntagSelectionComponent : Component
|
||||
[DataField]
|
||||
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
|
||||
|
||||
/// <summary>
|
||||
/// Cached sessions of antag definitions and selected players. Players in this dict are not guaranteed to have been assigned the role yet.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<AntagSelectionDefinition, HashSet<ICommonSession>>PreSelectedSessions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick.
|
||||
/// Is not serialized.
|
||||
/// </summary>
|
||||
public HashSet<ICommonSession> SelectedSessions = new();
|
||||
public HashSet<ICommonSession> AssignedSessions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Locale id for the name of the antag.
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Paper;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Server-side class implementing the core UI logic of NanoTask
|
||||
/// </summary>
|
||||
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem
|
||||
{
|
||||
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PaperSystem _paper = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
|
||||
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
|
||||
|
||||
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
|
||||
|
||||
SubscribeLocalEvent<NanoTaskInteractionComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
}
|
||||
|
||||
private void OnCartridgeRemoved(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeRemovedEvent args)
|
||||
{
|
||||
if (!_cartridgeLoader.HasProgram<NanoTaskCartridgeComponent>(args.Loader))
|
||||
{
|
||||
RemComp<NanoTaskInteractionComponent>(args.Loader);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInteractUsing(Entity<NanoTaskInteractionComponent> ent, ref InteractUsingEvent args)
|
||||
{
|
||||
if (!_cartridgeLoader.TryGetProgram<NanoTaskCartridgeComponent>(ent.Owner, out var uid, out var program))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!EntityManager.TryGetComponent<NanoTaskPrintedComponent>(args.Used, out var printed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (printed.Task is NanoTaskItem item)
|
||||
{
|
||||
program.Tasks.Add(new(program.Counter++, printed.Task));
|
||||
args.Handled = true;
|
||||
EntityManager.DeleteEntity(args.Used);
|
||||
UpdateUiState(new Entity<NanoTaskCartridgeComponent>(uid.Value, program), ent.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This gets called when the ui fragment needs to be updated for the first time after activating
|
||||
/// </summary>
|
||||
private void OnUiReady(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
|
||||
{
|
||||
UpdateUiState(ent, args.Loader);
|
||||
}
|
||||
|
||||
private void SetupPrintedTask(EntityUid uid, NanoTaskItem item)
|
||||
{
|
||||
PaperComponent? paper = null;
|
||||
NanoTaskPrintedComponent? printed = null;
|
||||
if (!Resolve(uid, ref paper, ref printed))
|
||||
return;
|
||||
|
||||
printed.Task = item;
|
||||
var msg = new FormattedMessage();
|
||||
msg.AddText(Loc.GetString("nano-task-printed-description", ("description", item.Description)));
|
||||
msg.PushNewline();
|
||||
msg.AddText(Loc.GetString("nano-task-printed-requester", ("requester", item.TaskIsFor)));
|
||||
msg.PushNewline();
|
||||
msg.AddText(item.Priority switch {
|
||||
NanoTaskPriority.High => Loc.GetString("nano-task-printed-high-priority"),
|
||||
NanoTaskPriority.Medium => Loc.GetString("nano-task-printed-medium-priority"),
|
||||
NanoTaskPriority.Low => Loc.GetString("nano-task-printed-low-priority"),
|
||||
_ => "",
|
||||
});
|
||||
|
||||
_paper.SetContent((uid, paper), msg.ToMarkup());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ui messages received here get wrapped by a CartridgeMessageEvent and are relayed from the <see cref="CartridgeLoaderSystem"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The cartridge specific ui message event needs to inherit from the CartridgeMessageEvent
|
||||
/// </remarks>
|
||||
private void OnUiMessage(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeMessageEvent args)
|
||||
{
|
||||
if (args is not NanoTaskUiMessageEvent message)
|
||||
return;
|
||||
|
||||
switch (message.Payload)
|
||||
{
|
||||
case NanoTaskAddTask task:
|
||||
if (!task.Item.Validate())
|
||||
return;
|
||||
|
||||
ent.Comp.Tasks.Add(new(ent.Comp.Counter++, task.Item));
|
||||
break;
|
||||
case NanoTaskUpdateTask task:
|
||||
{
|
||||
if (!task.Item.Data.Validate())
|
||||
return;
|
||||
|
||||
var idx = ent.Comp.Tasks.FindIndex(t => t.Id == task.Item.Id);
|
||||
if (idx != -1)
|
||||
ent.Comp.Tasks[idx] = task.Item;
|
||||
break;
|
||||
}
|
||||
case NanoTaskDeleteTask task:
|
||||
ent.Comp.Tasks.RemoveAll(t => t.Id == task.Id);
|
||||
break;
|
||||
case NanoTaskPrintTask task:
|
||||
{
|
||||
if (!task.Item.Validate())
|
||||
return;
|
||||
if (_timing.CurTime < ent.Comp.NextPrintAllowedAfter)
|
||||
return;
|
||||
|
||||
ent.Comp.NextPrintAllowedAfter = _timing.CurTime + ent.Comp.PrintDelay;
|
||||
var printed = Spawn("PaperNanoTaskItem", Transform(message.Actor).Coordinates);
|
||||
_hands.PickupOrDrop(message.Actor, printed);
|
||||
_audio.PlayPvs(new SoundPathSpecifier("/Audio/Machines/printer.ogg"), ent.Owner);
|
||||
SetupPrintedTask(printed, task.Item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateUiState(ent, GetEntity(args.LoaderUid));
|
||||
}
|
||||
|
||||
|
||||
private void UpdateUiState(Entity<NanoTaskCartridgeComponent> ent, EntityUid loaderUid)
|
||||
{
|
||||
var state = new NanoTaskUiState(ent.Comp.Tasks);
|
||||
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,15 @@ using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Implants;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.NameModifier.Components;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -28,6 +34,10 @@ public sealed class CloningSystem : EntitySystem
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly SharedStorageSystem _storage = default!;
|
||||
[Dependency] private readonly SharedStackSystem _stack = default!;
|
||||
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
|
||||
@@ -81,6 +91,15 @@ public sealed class CloningSystem : EntitySystem
|
||||
if (settings.CopyEquipment != null)
|
||||
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
|
||||
|
||||
// Copy storage on the mob itself as well.
|
||||
// This is needed for slime storage.
|
||||
if (settings.CopyInternalStorage)
|
||||
CopyStorage(original, clone.Value, settings.Whitelist, settings.Blacklist);
|
||||
|
||||
// copy implants and their storage contents
|
||||
if (settings.CopyImplants)
|
||||
CopyImplants(original, clone.Value, settings.CopyInternalStorage, settings.Whitelist, settings.Blacklist);
|
||||
|
||||
var originalName = Name(original);
|
||||
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
|
||||
originalName = nameModComp.BaseName;
|
||||
@@ -100,24 +119,122 @@ public sealed class CloningSystem : EntitySystem
|
||||
/// Copies the equipment the original has to the clone.
|
||||
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
|
||||
/// </summary>
|
||||
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
public void CopyEquipment(Entity<InventoryComponent?> original, Entity<InventoryComponent?> clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
{
|
||||
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
|
||||
if (!Resolve(original, ref original.Comp) || !Resolve(clone, ref clone.Comp))
|
||||
return;
|
||||
|
||||
var coords = Transform(clone).Coordinates;
|
||||
|
||||
// Iterate over all inventory slots
|
||||
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
|
||||
var slotEnumerator = _inventory.GetSlotEnumerator(original, slotFlags);
|
||||
while (slotEnumerator.NextItem(out var item, out var slot))
|
||||
{
|
||||
// Spawn a copy of the item using the original prototype.
|
||||
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
|
||||
// we use a whitelist and blacklist to be sure to exclude any problematic entities
|
||||
var cloneItem = CopyItem(item, coords, whitelist, blacklist);
|
||||
|
||||
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
|
||||
continue;
|
||||
|
||||
var prototype = MetaData(item).EntityPrototype;
|
||||
if (prototype != null)
|
||||
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
|
||||
if (cloneItem != null && !_inventory.TryEquip(clone, cloneItem.Value, slot.Name, silent: true, inventory: clone.Comp))
|
||||
Del(cloneItem); // delete it again if the clone cannot equip it
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies an item and its storage recursively, placing all items at the same position in grid storage.
|
||||
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not perfect and only considers item in storage containers.
|
||||
/// Some components have their own additional spawn logic on map init, so we cannot just copy all containers.
|
||||
/// </remarks>
|
||||
public EntityUid? CopyItem(EntityUid original, EntityCoordinates coords, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
{
|
||||
// we use a whitelist and blacklist to be sure to exclude any problematic entities
|
||||
if (!_whitelist.CheckBoth(original, blacklist, whitelist))
|
||||
return null;
|
||||
|
||||
var prototype = MetaData(original).EntityPrototype?.ID;
|
||||
if (prototype == null)
|
||||
return null;
|
||||
|
||||
var spawned = EntityManager.SpawnAtPosition(prototype, coords);
|
||||
|
||||
// if the original is a stack, adjust the count of the copy
|
||||
if (TryComp<StackComponent>(original, out var originalStack) && TryComp<StackComponent>(spawned, out var spawnedStack))
|
||||
_stack.SetCount(spawned, originalStack.Count, spawnedStack);
|
||||
|
||||
// if the original has items inside its storage, copy those as well
|
||||
if (TryComp<StorageComponent>(original, out var originalStorage) && TryComp<StorageComponent>(spawned, out var spawnedStorage))
|
||||
{
|
||||
// remove all items that spawned with the entity inside its storage
|
||||
// this ignores other containers, but this should be good enough for our purposes
|
||||
_container.CleanContainer(spawnedStorage.Container);
|
||||
|
||||
// recursively replace them
|
||||
// surely no one will ever create two items that contain each other causing an infinite loop, right?
|
||||
foreach ((var itemUid, var itemLocation) in originalStorage.StoredItems)
|
||||
{
|
||||
var copy = CopyItem(itemUid, coords, whitelist, blacklist);
|
||||
if (copy != null)
|
||||
_storage.InsertAt((spawned, spawnedStorage), copy.Value, itemLocation, out _, playSound: false);
|
||||
}
|
||||
}
|
||||
|
||||
return spawned;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies an item's storage recursively to another storage.
|
||||
/// The storage grids should have the same shape or it will drop on the floor.
|
||||
/// Basically the same as CopyItem, but we don't copy the outermost container.
|
||||
/// </summary>
|
||||
public void CopyStorage(Entity<StorageComponent?> original, Entity<StorageComponent?> target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
{
|
||||
if (!Resolve(original, ref original.Comp, false) || !Resolve(target, ref target.Comp, false))
|
||||
return;
|
||||
|
||||
var coords = Transform(target).Coordinates;
|
||||
|
||||
// delete all items in the target storage
|
||||
_container.CleanContainer(target.Comp.Container);
|
||||
|
||||
// recursively replace them
|
||||
foreach ((var itemUid, var itemLocation) in original.Comp.StoredItems)
|
||||
{
|
||||
var copy = CopyItem(itemUid, coords, whitelist, blacklist);
|
||||
if (copy != null)
|
||||
_storage.InsertAt(target, copy.Value, itemLocation, out _, playSound: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies all implants from one mob to another.
|
||||
/// Might result in duplicates if the target already has them.
|
||||
/// Can copy the storage inside a storage implant according to a whitelist and blacklist.
|
||||
/// </summary>
|
||||
/// <param name="original">Entity to copy implants from.</param>
|
||||
/// <param name="target">Entity to copy implants to.</param>
|
||||
/// <param name="copyStorage">If true will copy storage of the implants (E.g storage implant)</param>
|
||||
/// <param name="whitelist">Whitelist for the storage copy (If copyStorage is true)</param>
|
||||
/// <param name="blacklist">Blacklist for the storage copy (If copyStorage is true)</param>
|
||||
public void CopyImplants(Entity<ImplantedComponent?> original, EntityUid target, bool copyStorage = false, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
{
|
||||
if (!Resolve(original, ref original.Comp, false))
|
||||
return; // they don't have any implants to copy!
|
||||
|
||||
foreach (var originalImplant in original.Comp.ImplantContainer.ContainedEntities)
|
||||
{
|
||||
if (!HasComp<SubdermalImplantComponent>(originalImplant))
|
||||
continue; // not an implant (should only happen with admin shenanigans)
|
||||
|
||||
var implantId = MetaData(originalImplant).EntityPrototype?.ID;
|
||||
|
||||
if (implantId == null)
|
||||
continue;
|
||||
|
||||
var targetImplant = _subdermalImplant.AddImplant(target, implantId);
|
||||
|
||||
if (copyStorage && targetImplant != null)
|
||||
CopyStorage(originalImplant, targetImplant.Value, whitelist, blacklist); // only needed for storage implants
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a protoype when triggered.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(TriggerSystem))]
|
||||
public sealed partial class SpawnOnTriggerComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string Proto = string.Empty;
|
||||
/// <summary>
|
||||
/// The prototype to spawn.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Proto = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Use MapCoordinates for spawning?
|
||||
/// Set to true if you don't want the new entity parented to the spawner.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool mapCoords;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
namespace Content.Server.Explosion.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class TriggerOnCollideComponent : Component
|
||||
{
|
||||
[DataField("fixtureID", required: true)]
|
||||
public string FixtureID = String.Empty;
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Doesn't trigger if the other colliding fixture is nonhard.
|
||||
/// </summary>
|
||||
[DataField("ignoreOtherNonHard")]
|
||||
public bool IgnoreOtherNonHard = true;
|
||||
}
|
||||
/// <summary>
|
||||
/// Triggers when colliding with another entity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class TriggerOnCollideComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The fixture with which to collide.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string FixtureID = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Doesn't trigger if the other colliding fixture is nonhard.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool IgnoreOtherNonHard = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers on use in hand.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class TriggerOnUseComponent : Component { }
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Whitelist;
|
||||
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user of a Trigger satisfies a whitelist and blacklist condition.
|
||||
/// Cancels the trigger otherwise.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class TriggerWhitelistComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whitelist for what entites can cause this trigger.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
/// <summary>
|
||||
/// Blacklist for what entites can cause this trigger.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using Content.Shared.Explosion.Components;
|
||||
using Content.Shared.Explosion.Components.OnTrigger;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
@@ -23,6 +24,7 @@ using Content.Shared.Slippery;
|
||||
using Content.Shared.StepTrigger.Systems;
|
||||
using Content.Shared.Trigger;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Content.Shared.Whitelist;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
@@ -31,10 +33,7 @@ using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.Coordinates;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems
|
||||
{
|
||||
@@ -53,6 +52,12 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised before a trigger is activated.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct BeforeTriggerEvent(EntityUid Triggered, EntityUid? User, bool Cancelled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when timer trigger becomes active.
|
||||
/// </summary>
|
||||
@@ -78,6 +83,7 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -93,6 +99,7 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnTriggered);
|
||||
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
|
||||
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<TriggerOnUseComponent, UseInHandEvent>(OnUse);
|
||||
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
|
||||
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
|
||||
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
|
||||
@@ -109,6 +116,13 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
|
||||
SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
|
||||
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
|
||||
|
||||
SubscribeLocalEvent<TriggerWhitelistComponent, BeforeTriggerEvent>(HandleWhitelist);
|
||||
}
|
||||
|
||||
private void HandleWhitelist(Entity<TriggerWhitelistComponent> ent, ref BeforeTriggerEvent args)
|
||||
{
|
||||
args.Cancelled = !_whitelist.CheckBoth(args.User, ent.Comp.Blacklist, ent.Comp.Whitelist);
|
||||
}
|
||||
|
||||
private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args)
|
||||
@@ -155,16 +169,23 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
RemCompDeferred<AnchorOnTriggerComponent>(uid);
|
||||
}
|
||||
|
||||
private void OnSpawnTrigger(EntityUid uid, SpawnOnTriggerComponent component, TriggerEvent args)
|
||||
private void OnSpawnTrigger(Entity<SpawnOnTriggerComponent> ent, ref TriggerEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var xform = Transform(ent);
|
||||
|
||||
var coords = xform.Coordinates;
|
||||
if (ent.Comp.mapCoords)
|
||||
{
|
||||
var mapCoords = _transformSystem.GetMapCoordinates(ent, xform);
|
||||
Spawn(ent.Comp.Proto, mapCoords);
|
||||
}
|
||||
else
|
||||
{
|
||||
var coords = xform.Coordinates;
|
||||
if (!coords.IsValid(EntityManager))
|
||||
return;
|
||||
Spawn(ent.Comp.Proto, coords);
|
||||
|
||||
if (!coords.IsValid(EntityManager))
|
||||
return;
|
||||
|
||||
Spawn(component.Proto, coords);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
|
||||
@@ -248,6 +269,15 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnUse(Entity<TriggerOnUseComponent> ent, ref UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
Trigger(ent.Owner, args.User);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
|
||||
{
|
||||
args.Handled = Trigger(uid);
|
||||
@@ -275,6 +305,11 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
|
||||
public bool Trigger(EntityUid trigger, EntityUid? user = null)
|
||||
{
|
||||
var beforeTriggerEvent = new BeforeTriggerEvent(trigger, user);
|
||||
RaiseLocalEvent(trigger, ref beforeTriggerEvent);
|
||||
if (beforeTriggerEvent.Cancelled)
|
||||
return false;
|
||||
|
||||
var triggerEvent = new TriggerEvent(trigger, user);
|
||||
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true);
|
||||
return triggerEvent.Handled;
|
||||
|
||||
@@ -73,7 +73,6 @@ namespace Content.Server.Forensics
|
||||
|
||||
private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||
if (ent.Comp.DNA == null)
|
||||
RandomizeDNA((ent.Owner, ent.Comp));
|
||||
else
|
||||
@@ -327,7 +326,6 @@ namespace Content.Server.Forensics
|
||||
ent.Comp.DNA = GenerateDNA();
|
||||
Dirty(ent);
|
||||
|
||||
Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||
RaiseLocalEvent(ent.Owner, ref ev);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Gamerule component for spawning a paradox clone antagonist.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ParadoxCloneRuleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Cloning settings to be used.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
|
||||
|
||||
/// <summary>
|
||||
/// Visual effect spawned when gibbing at round end.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId GibProto = "MobParadoxTimed";
|
||||
|
||||
/// <summary>
|
||||
/// Mind entity of the original player.
|
||||
/// Gets assigned when cloning.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? Original;
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist for Objectives to be copied to the clone.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? ObjectiveWhitelist;
|
||||
|
||||
/// <summary>
|
||||
/// Blacklist for Objectives to be copied to the clone.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? ObjectiveBlacklist;
|
||||
}
|
||||
@@ -19,6 +19,11 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
|
||||
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
|
||||
}
|
||||
|
||||
protected EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent> QueryDelayedRules()
|
||||
{
|
||||
return EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries all gamerules, regardless of if they're active or not.
|
||||
/// </summary>
|
||||
|
||||
94
Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs
Normal file
94
Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Cloning;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Medical.SuitSensors;
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Gibbing.Components;
|
||||
using Content.Shared.Medical.SuitSensor;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||
[Dependency] private readonly SuitSensorSystem _sensor = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ParadoxCloneRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
|
||||
SubscribeLocalEvent<ParadoxCloneRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagEntitySelected);
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, ParadoxCloneRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
// check if we got enough potential cloning targets, otherwise cancel the gamerule so that the ghost role does not show up
|
||||
var allHumans = _mind.GetAliveHumans();
|
||||
|
||||
if (allHumans.Count == 0)
|
||||
{
|
||||
Log.Info("Could not find any alive players to create a paradox clone from! Ending gamerule.");
|
||||
ForceEndSelf(uid, gameRule);
|
||||
}
|
||||
}
|
||||
|
||||
// we have to do the spawning here so we can transfer the mind to the correct entity and can assign the objectives correctly
|
||||
private void OnAntagSelectEntity(Entity<ParadoxCloneRuleComponent> ent, ref AntagSelectEntityEvent args)
|
||||
{
|
||||
if (args.Session?.AttachedEntity is not { } spawner)
|
||||
return;
|
||||
|
||||
// get possible targets
|
||||
var allHumans = _mind.GetAliveHumans();
|
||||
|
||||
// we already checked when starting the gamerule, but someone might have died since then.
|
||||
if (allHumans.Count == 0)
|
||||
{
|
||||
Log.Warning("Could not find any alive players to create a paradox clone from!");
|
||||
return;
|
||||
}
|
||||
|
||||
// pick a random player
|
||||
var playerToClone = _random.Pick(allHumans);
|
||||
var bodyToClone = playerToClone.Comp.OwnedEntity;
|
||||
|
||||
if (bodyToClone == null || !_cloning.TryCloning(bodyToClone.Value, _transform.GetMapCoordinates(spawner), ent.Comp.Settings, out var clone))
|
||||
{
|
||||
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(bodyToClone)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var targetComp = EnsureComp<TargetOverrideComponent>(clone.Value);
|
||||
targetComp.Target = playerToClone.Owner; // set the kill target
|
||||
|
||||
var gibComp = EnsureComp<GibOnRoundEndComponent>(clone.Value);
|
||||
gibComp.SpawnProto = ent.Comp.GibProto;
|
||||
gibComp.PreventGibbingObjectives = new() { "ParadoxCloneKillObjective" }; // don't gib them if they killed the original.
|
||||
|
||||
// turn their suit sensors off so they don't immediately get noticed
|
||||
_sensor.SetAllSensors(clone.Value, SuitSensorMode.SensorOff);
|
||||
|
||||
args.Entity = clone;
|
||||
ent.Comp.Original = playerToClone.Owner;
|
||||
}
|
||||
|
||||
private void AfterAntagEntitySelected(Entity<ParadoxCloneRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
if (ent.Comp.Original == null)
|
||||
return;
|
||||
|
||||
if (!_mind.TryGetMind(args.EntityUid, out var cloneMindId, out var cloneMindComp))
|
||||
return;
|
||||
|
||||
_mind.CopyObjectives(ent.Comp.Original.Value, (cloneMindId, cloneMindComp), ent.Comp.ObjectiveWhitelist, ent.Comp.ObjectiveBlacklist);
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
commandList.Add(id);
|
||||
}
|
||||
|
||||
return IsGroupDetainedOrDead(commandList, true, true);
|
||||
return IsGroupDetainedOrDead(commandList, true, true, true);
|
||||
}
|
||||
|
||||
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
|
||||
@@ -214,7 +214,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
|
||||
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
|
||||
// Cuffing Head Revs is not enough - they must be killed.
|
||||
if (IsGroupDetainedOrDead(headRevList, false, false))
|
||||
if (IsGroupDetainedOrDead(headRevList, false, false, false))
|
||||
{
|
||||
var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>();
|
||||
while (rev.MoveNext(out var uid, out _, out var mc))
|
||||
@@ -251,34 +251,45 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
/// <param name="list">The list of the entities</param>
|
||||
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them missing in action. (Won't check when emergency shuttle arrives just in case)</param>
|
||||
/// <param name="countCuffed">Bool for if you don't want to count cuffed entities.</param>
|
||||
/// <param name="countRevolutionaries">Bool for if you want to count revolutionaries.</param>
|
||||
/// <returns></returns>
|
||||
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed)
|
||||
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed, bool countRevolutionaries)
|
||||
{
|
||||
var gone = 0;
|
||||
|
||||
foreach (var entity in list)
|
||||
{
|
||||
if (TryComp<CuffableComponent>(entity, out var cuffed) && cuffed.CuffedHandCount > 0 && countCuffed)
|
||||
{
|
||||
gone++;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
|
||||
if (TryComp<MobStateComponent>(entity, out var state))
|
||||
{
|
||||
if (TryComp<MobStateComponent>(entity, out var state))
|
||||
{
|
||||
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
|
||||
{
|
||||
gone++;
|
||||
}
|
||||
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
|
||||
{
|
||||
gone++;
|
||||
}
|
||||
}
|
||||
//If they don't have the MobStateComponent they might as well be dead.
|
||||
else
|
||||
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
|
||||
{
|
||||
gone++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
|
||||
{
|
||||
gone++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
//If they don't have the MobStateComponent they might as well be dead.
|
||||
else
|
||||
{
|
||||
gone++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((HasComp<RevolutionaryComponent>(entity) || HasComp<HeadRevolutionaryComponent>(entity)) && countRevolutionaries)
|
||||
{
|
||||
gone++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
55
Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs
Normal file
55
Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Gibbing.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Content.Server.Body.Systems;
|
||||
|
||||
namespace Content.Server.Gibbing.Systems;
|
||||
public sealed class GibOnRoundEndSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly BodySystem _body = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// this is raised after RoundEndTextAppendEvent, so they can successfully greentext before we gib them
|
||||
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEnd);
|
||||
}
|
||||
|
||||
private void OnRoundEnd(RoundEndMessageEvent args)
|
||||
{
|
||||
var gibQuery = EntityQueryEnumerator<GibOnRoundEndComponent>();
|
||||
|
||||
// gib everyone with the component
|
||||
while (gibQuery.MoveNext(out var uid, out var gibComp))
|
||||
{
|
||||
var gib = false;
|
||||
// if they fulfill all objectives given in the component they are not gibbed
|
||||
if (_mind.TryGetMind(uid, out var mindId, out var mindComp))
|
||||
{
|
||||
foreach (var objectiveId in gibComp.PreventGibbingObjectives)
|
||||
{
|
||||
if (!_mind.TryFindObjective((mindId, mindComp), objectiveId, out var objective)
|
||||
|| !_objectives.IsCompleted(objective.Value, (mindId, mindComp)))
|
||||
{
|
||||
gib = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
gib = true;
|
||||
|
||||
if (!gib)
|
||||
continue;
|
||||
|
||||
if (gibComp.SpawnProto != null)
|
||||
SpawnAtPosition(gibComp.SpawnProto, Transform(uid).Coordinates);
|
||||
|
||||
_body.GibBody(uid, splatModifier: 5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Medical.SuitSensor;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
@@ -44,6 +45,7 @@ public sealed class SuitSensorSystem : EntitySystem
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -347,6 +349,20 @@ public sealed class SuitSensorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set all suit sensors on the equipment someone is wearing to the specified mode.
|
||||
/// </summary>
|
||||
public void SetAllSensors(EntityUid target, SuitSensorMode mode, SlotFlags slots = SlotFlags.All )
|
||||
{
|
||||
// iterate over all inventory slots
|
||||
var slotEnumerator = _inventory.GetSlotEnumerator(target, slots);
|
||||
while (slotEnumerator.NextItem(out var item, out _))
|
||||
{
|
||||
if (TryComp<SuitSensorComponent>(item, out var sensorComp))
|
||||
SetSensor((item, sensorComp), mode);
|
||||
}
|
||||
}
|
||||
|
||||
public SuitSensorStatus? GetSensorState(EntityUid uid, SuitSensorComponent? sensor = null, TransformComponent? transform = null)
|
||||
{
|
||||
if (!Resolve(uid, ref sensor, ref transform))
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Shared.Implants.Components;
|
||||
using Content.Shared.Mindshield.Components;
|
||||
using Content.Shared.Revolutionary.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.Mindshield;
|
||||
|
||||
@@ -29,6 +30,7 @@ public sealed class MindShieldSystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
|
||||
SubscribeLocalEvent<MindShieldImplantComponent, EntGotRemovedFromContainerMessage>(OnImplantDraw);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,4 +63,10 @@ public sealed class MindShieldSystem : EntitySystem
|
||||
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted)} was deconverted due to being implanted with a Mindshield.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnImplantDraw(Entity<MindShieldImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
|
||||
{
|
||||
RemComp<MindShieldComponent>(args.Container.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
|
||||
/// If there are no heads it will fallback to any person.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
|
||||
public sealed partial class PickRandomHeadComponent : Component
|
||||
{
|
||||
}
|
||||
[RegisterComponent]
|
||||
public sealed partial class PickRandomHeadComponent : Component;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
|
||||
public sealed partial class PickRandomPersonComponent : Component
|
||||
{
|
||||
}
|
||||
[RegisterComponent]
|
||||
public sealed partial class PickRandomPersonComponent : Component;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Sets this objective's target to the one given in <see cref="TargetOverrideComponent"/>, if the entity has it.
|
||||
/// This component needs to be added to objective entity itself.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class PickSpecificPersonComponent : Component;
|
||||
@@ -1,11 +1,7 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
|
||||
public sealed partial class RandomTraitorAliveComponent : Component
|
||||
{
|
||||
}
|
||||
[RegisterComponent]
|
||||
public sealed partial class RandomTraitorAliveComponent : Component;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
|
||||
public sealed partial class RandomTraitorProgressComponent : Component
|
||||
{
|
||||
}
|
||||
[RegisterComponent]
|
||||
public sealed partial class RandomTraitorProgressComponent : Component;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Sets a target objective to a specific target when receiving it.
|
||||
/// The objective entity needs to have <see cref="PickSpecificPersonComponent"/>.
|
||||
/// This component needs to be added to entity receiving the objective.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class TargetOverrideComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity that should be targeted.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? Target;
|
||||
}
|
||||
@@ -1,31 +1,23 @@
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles help progress condition logic and picking random help targets.
|
||||
/// Handles help progress condition logic.
|
||||
/// </summary>
|
||||
public sealed class HelpProgressConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HelpProgressConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
|
||||
SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnTraitorAssigned);
|
||||
}
|
||||
|
||||
private void OnGetProgress(EntityUid uid, HelpProgressConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||
@@ -36,55 +28,6 @@ public sealed class HelpProgressConditionSystem : EntitySystem
|
||||
args.Progress = GetProgress(target.Value);
|
||||
}
|
||||
|
||||
private void OnTraitorAssigned(EntityUid uid, RandomTraitorProgressComponent comp, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
|
||||
|
||||
// cant help anyone who is tasked with helping:
|
||||
// 1. thats boring
|
||||
// 2. no cyclic progress dependencies!!!
|
||||
foreach (var traitor in traitors)
|
||||
{
|
||||
// TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
|
||||
if (!TryComp<MindComponent>(traitor.Id, out var mind))
|
||||
continue;
|
||||
|
||||
foreach (var objective in mind.Objectives)
|
||||
{
|
||||
if (HasComp<HelpProgressConditionComponent>(objective))
|
||||
traitors.RemoveWhere(x => x.Mind == mind);
|
||||
}
|
||||
}
|
||||
|
||||
// Can't have multiple objectives to help/save the same person
|
||||
foreach (var objective in args.Mind.Objectives)
|
||||
{
|
||||
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
|
||||
{
|
||||
if (TryComp<TargetObjectiveComponent>(objective, out var help))
|
||||
{
|
||||
traitors.RemoveWhere(x => x.Id == help.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no more helpable traitors
|
||||
if (traitors.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_target.SetTarget(uid, _random.Pick(traitors).Id, target);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid target)
|
||||
{
|
||||
var total = 0f; // how much progress they have
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles keep alive condition logic and picking random traitors to keep alive.
|
||||
/// Handles keep alive condition logic.
|
||||
/// </summary>
|
||||
public sealed class KeepAliveConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<KeepAliveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
|
||||
SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnAssigned);
|
||||
}
|
||||
|
||||
private void OnGetProgress(EntityUid uid, KeepAliveConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||
@@ -35,39 +27,6 @@ public sealed class KeepAliveConditionSystem : EntitySystem
|
||||
args.Progress = GetProgress(target.Value);
|
||||
}
|
||||
|
||||
private void OnAssigned(EntityUid uid, RandomTraitorAliveComponent comp, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
|
||||
|
||||
// Can't have multiple objectives to help/save the same person
|
||||
foreach (var objective in args.Mind.Objectives)
|
||||
{
|
||||
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
|
||||
{
|
||||
if (TryComp<TargetObjectiveComponent>(objective, out var help))
|
||||
{
|
||||
traitors.RemoveWhere(x => x.Id == help.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// You are the first/only traitor.
|
||||
if (traitors.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_target.SetTarget(uid, _random.Pick(traitors).Id, target);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid target)
|
||||
{
|
||||
if (!TryComp<MindComponent>(target, out var mind))
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Revolutionary.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
@@ -17,7 +14,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
|
||||
@@ -26,10 +22,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<KillPersonConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
|
||||
SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnPersonAssigned);
|
||||
|
||||
SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnHeadAssigned);
|
||||
}
|
||||
|
||||
private void OnGetProgress(EntityUid uid, KillPersonConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||
@@ -40,74 +32,6 @@ public sealed class KillPersonConditionSystem : EntitySystem
|
||||
args.Progress = GetProgress(target.Value, comp.RequireDead);
|
||||
}
|
||||
|
||||
private void OnPersonAssigned(EntityUid uid, PickRandomPersonComponent comp, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid objective prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// target already assigned
|
||||
if (target.Target != null)
|
||||
return;
|
||||
|
||||
var allHumans = _mind.GetAliveHumans(args.MindId);
|
||||
|
||||
// Can't have multiple objectives to kill the same person
|
||||
foreach (var objective in args.Mind.Objectives)
|
||||
{
|
||||
if (HasComp<KillPersonConditionComponent>(objective) && TryComp<TargetObjectiveComponent>(objective, out var kill))
|
||||
{
|
||||
allHumans.RemoveWhere(x => x.Owner == kill.Target);
|
||||
}
|
||||
}
|
||||
|
||||
// no other humans to kill
|
||||
if (allHumans.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_target.SetTarget(uid, _random.Pick(allHumans), target);
|
||||
}
|
||||
|
||||
private void OnHeadAssigned(EntityUid uid, PickRandomHeadComponent comp, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// target already assigned
|
||||
if (target.Target != null)
|
||||
return;
|
||||
|
||||
// no other humans to kill
|
||||
var allHumans = _mind.GetAliveHumans(args.MindId);
|
||||
if (allHumans.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var allHeads = new HashSet<Entity<MindComponent>>();
|
||||
foreach (var person in allHumans)
|
||||
{
|
||||
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } ent && HasComp<CommandStaffComponent>(ent))
|
||||
allHeads.Add(person);
|
||||
}
|
||||
|
||||
if (allHeads.Count == 0)
|
||||
allHeads = allHumans; // fallback to non-head target
|
||||
|
||||
_target.SetTarget(uid, _random.Pick(allHeads), target);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid target, bool requireDead)
|
||||
{
|
||||
// deleted or gibbed or something, counts as dead
|
||||
|
||||
212
Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs
Normal file
212
Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Revolutionary.Components;
|
||||
using Robust.Shared.Random;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles assinging a target to an objective entity with <see cref="TargetObjectiveComponent"/> using different components.
|
||||
/// These can be combined with condition components for objective completions in order to create a variety of objectives.
|
||||
/// </summary>
|
||||
public sealed class PickObjectiveTargetSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PickSpecificPersonComponent, ObjectiveAssignedEvent>(OnSpecificPersonAssigned);
|
||||
SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnRandomPersonAssigned);
|
||||
SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnRandomHeadAssigned);
|
||||
|
||||
SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnRandomTraitorProgressAssigned);
|
||||
SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnRandomTraitorAliveAssigned);
|
||||
}
|
||||
|
||||
private void OnSpecificPersonAssigned(Entity<PickSpecificPersonComponent> ent, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid objective prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// target already assigned
|
||||
if (target.Target != null)
|
||||
return;
|
||||
|
||||
if (args.Mind.OwnedEntity == null)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var user = args.Mind.OwnedEntity.Value;
|
||||
if (!TryComp<TargetOverrideComponent>(user, out var targetComp) || targetComp.Target == null)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_target.SetTarget(ent.Owner, targetComp.Target.Value);
|
||||
}
|
||||
|
||||
private void OnRandomPersonAssigned(Entity<PickRandomPersonComponent> ent, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid objective prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// target already assigned
|
||||
if (target.Target != null)
|
||||
return;
|
||||
|
||||
var allHumans = _mind.GetAliveHumans(args.MindId);
|
||||
|
||||
// Can't have multiple objectives to kill the same person
|
||||
foreach (var objective in args.Mind.Objectives)
|
||||
{
|
||||
if (HasComp<KillPersonConditionComponent>(objective) && TryComp<TargetObjectiveComponent>(objective, out var kill))
|
||||
{
|
||||
allHumans.RemoveWhere(x => x.Owner == kill.Target);
|
||||
}
|
||||
}
|
||||
|
||||
// no other humans to kill
|
||||
if (allHumans.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_target.SetTarget(ent.Owner, _random.Pick(allHumans), target);
|
||||
}
|
||||
|
||||
private void OnRandomHeadAssigned(Entity<PickRandomHeadComponent> ent, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// target already assigned
|
||||
if (target.Target != null)
|
||||
return;
|
||||
|
||||
// no other humans to kill
|
||||
var allHumans = _mind.GetAliveHumans(args.MindId);
|
||||
if (allHumans.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var allHeads = new HashSet<Entity<MindComponent>>();
|
||||
foreach (var person in allHumans)
|
||||
{
|
||||
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } owned && HasComp<CommandStaffComponent>(owned))
|
||||
allHeads.Add(person);
|
||||
}
|
||||
|
||||
if (allHeads.Count == 0)
|
||||
allHeads = allHumans; // fallback to non-head target
|
||||
|
||||
_target.SetTarget(ent.Owner, _random.Pick(allHeads), target);
|
||||
}
|
||||
|
||||
private void OnRandomTraitorProgressAssigned(Entity<RandomTraitorProgressComponent> ent, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
|
||||
|
||||
// cant help anyone who is tasked with helping:
|
||||
// 1. thats boring
|
||||
// 2. no cyclic progress dependencies!!!
|
||||
foreach (var traitor in traitors)
|
||||
{
|
||||
// TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
|
||||
if (!TryComp<MindComponent>(traitor.Id, out var mind))
|
||||
continue;
|
||||
|
||||
foreach (var objective in mind.Objectives)
|
||||
{
|
||||
if (HasComp<HelpProgressConditionComponent>(objective))
|
||||
traitors.RemoveWhere(x => x.Mind == mind);
|
||||
}
|
||||
}
|
||||
|
||||
// Can't have multiple objectives to help/save the same person
|
||||
foreach (var objective in args.Mind.Objectives)
|
||||
{
|
||||
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
|
||||
{
|
||||
if (TryComp<TargetObjectiveComponent>(objective, out var help))
|
||||
{
|
||||
traitors.RemoveWhere(x => x.Id == help.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no more helpable traitors
|
||||
if (traitors.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target);
|
||||
}
|
||||
|
||||
private void OnRandomTraitorAliveAssigned(Entity<RandomTraitorAliveComponent> ent, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
// invalid prototype
|
||||
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
|
||||
|
||||
// Can't have multiple objectives to help/save the same person
|
||||
foreach (var objective in args.Mind.Objectives)
|
||||
{
|
||||
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
|
||||
{
|
||||
if (TryComp<TargetObjectiveComponent>(objective, out var help))
|
||||
{
|
||||
traitors.RemoveWhere(x => x.Id == help.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// You are the first/only traitor.
|
||||
if (traitors.Count == 0)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Content.Shared.Polymorph;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Polymorph.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Intended for use with the trigger system.
|
||||
/// Polymorphs the user of the trigger.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class PolymorphOnTriggerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Polymorph settings.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<PolymorphPrototype> Polymorph;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using Content.Server.Polymorph.Components;
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Polymorph.Systems;
|
||||
|
||||
public partial class PolymorphSystem
|
||||
{
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Need to do this so we don't get a collection enumeration error in physics by polymorphing
|
||||
/// an entity we're colliding with
|
||||
/// </summary>
|
||||
private Queue<PolymorphQueuedData> _queuedPolymorphUpdates = new();
|
||||
|
||||
private void InitializeCollide()
|
||||
{
|
||||
SubscribeLocalEvent<PolymorphOnCollideComponent, StartCollideEvent>(OnPolymorphCollide);
|
||||
}
|
||||
|
||||
public void UpdateCollide()
|
||||
{
|
||||
while (_queuedPolymorphUpdates.TryDequeue(out var data))
|
||||
{
|
||||
if (Deleted(data.Ent))
|
||||
continue;
|
||||
|
||||
var ent = PolymorphEntity(data.Ent, data.Polymorph);
|
||||
if (ent != null)
|
||||
_audio.PlayPvs(data.Sound, ent.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPolymorphCollide(EntityUid uid, PolymorphOnCollideComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (args.OurFixtureId != SharedProjectileSystem.ProjectileFixture)
|
||||
return;
|
||||
|
||||
var other = args.OtherEntity;
|
||||
if (_whitelistSystem.IsWhitelistFail(component.Whitelist, other) ||
|
||||
_whitelistSystem.IsBlacklistPass(component.Blacklist, other))
|
||||
return;
|
||||
|
||||
_queuedPolymorphUpdates.Enqueue(new (other, component.Sound, component.Polymorph));
|
||||
}
|
||||
}
|
||||
|
||||
public struct PolymorphQueuedData
|
||||
{
|
||||
public EntityUid Ent;
|
||||
public SoundSpecifier Sound;
|
||||
public ProtoId<PolymorphPrototype> Polymorph;
|
||||
|
||||
public PolymorphQueuedData(EntityUid ent, SoundSpecifier sound, ProtoId<PolymorphPrototype> polymorph)
|
||||
{
|
||||
Ent = ent;
|
||||
Sound = sound;
|
||||
Polymorph = polymorph;
|
||||
}
|
||||
}
|
||||
41
Content.Server/Polymorph/Systems/PolymorphSystem.Trigger.cs
Normal file
41
Content.Server/Polymorph/Systems/PolymorphSystem.Trigger.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Server.Polymorph.Components;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Polymorph.Systems;
|
||||
|
||||
public sealed partial class PolymorphSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Need to do this so we don't get a collection enumeration error in physics by polymorphing
|
||||
/// an entity we're colliding with in case of TriggerOnCollide.
|
||||
/// Also makes sure other trigger effects don't activate in nullspace after we have polymorphed.
|
||||
/// </summary>
|
||||
private Queue<(EntityUid Ent, ProtoId<PolymorphPrototype> Polymorph)> _queuedPolymorphUpdates = new();
|
||||
|
||||
private void InitializeTrigger()
|
||||
{
|
||||
SubscribeLocalEvent<PolymorphOnTriggerComponent, TriggerEvent>(OnTrigger);
|
||||
}
|
||||
|
||||
private void OnTrigger(Entity<PolymorphOnTriggerComponent> ent, ref TriggerEvent args)
|
||||
{
|
||||
if (args.User == null)
|
||||
return;
|
||||
|
||||
_queuedPolymorphUpdates.Enqueue((args.User.Value, ent.Comp.Polymorph));
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void UpdateTrigger()
|
||||
{
|
||||
while (_queuedPolymorphUpdates.TryDequeue(out var data))
|
||||
{
|
||||
if (TerminatingOrDeleted(data.Item1))
|
||||
continue;
|
||||
|
||||
PolymorphEntity(data.Item1, data.Item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,8 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
|
||||
SubscribeLocalEvent<PolymorphedEntityComponent, DestructionEventArgs>(OnDestruction);
|
||||
|
||||
InitializeCollide();
|
||||
InitializeMap();
|
||||
InitializeTrigger();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -89,7 +89,7 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
UpdateCollide();
|
||||
UpdateTrigger();
|
||||
}
|
||||
|
||||
private void OnComponentStartup(Entity<PolymorphableComponent> ent, ref ComponentStartup args)
|
||||
@@ -204,6 +204,12 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
|
||||
var child = Spawn(configuration.Entity, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid));
|
||||
|
||||
if (configuration.PolymorphPopup != null)
|
||||
_popup.PopupEntity(Loc.GetString(configuration.PolymorphPopup,
|
||||
("parent", Identity.Entity(uid, EntityManager)),
|
||||
("child", Identity.Entity(child, EntityManager))),
|
||||
child);
|
||||
|
||||
MakeSentientCommand.MakeSentient(child, EntityManager);
|
||||
|
||||
var polymorphedComp = _compFact.GetComponent<PolymorphedEntityComponent>();
|
||||
@@ -347,10 +353,11 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
var ev = new PolymorphedEvent(uid, parent, true);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("polymorph-revert-popup-generic",
|
||||
if (component.Configuration.ExitPolymorphPopup != null)
|
||||
_popup.PopupEntity(Loc.GetString(component.Configuration.ExitPolymorphPopup,
|
||||
("parent", Identity.Entity(uid, EntityManager)),
|
||||
("child", Identity.Entity(parent, EntityManager))),
|
||||
parent);
|
||||
parent);
|
||||
QueueDel(uid);
|
||||
|
||||
return parent;
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.Dataset;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Pointing;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.RatKing;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
@@ -120,10 +121,10 @@ namespace Content.Server.RatKing
|
||||
base.DoCommandCallout(uid, component);
|
||||
|
||||
if (!component.OrderCallouts.TryGetValue(component.CurrentOrder, out var datasetId) ||
|
||||
!PrototypeManager.TryIndex<DatasetPrototype>(datasetId, out var datasetPrototype))
|
||||
!PrototypeManager.TryIndex<LocalizedDatasetPrototype>(datasetId, out var datasetPrototype))
|
||||
return;
|
||||
|
||||
var msg = Random.Pick(datasetPrototype.Values);
|
||||
var msg = Random.Pick(datasetPrototype);
|
||||
_chat.TrySendInGameICMessage(uid, msg, InGameICChatType.Speak, true);
|
||||
}
|
||||
}
|
||||
|
||||
9
Content.Server/Roles/ParadoxCloneRoleComponent.cs
Normal file
9
Content.Server/Roles/ParadoxCloneRoleComponent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Content.Shared.Roles;
|
||||
|
||||
namespace Content.Server.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// Added to mind role entities to tag that they are a paradox clone.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ParadoxCloneRoleComponent : BaseMindRoleComponent;
|
||||
@@ -19,12 +19,14 @@ public sealed partial class SalvageSystem
|
||||
private const string MagnetChannel = "Supply";
|
||||
|
||||
private EntityQuery<SalvageMobRestrictionsComponent> _salvMobQuery;
|
||||
private EntityQuery<MobStateComponent> _mobStateQuery;
|
||||
|
||||
private List<(Entity<TransformComponent> Entity, EntityUid MapUid, Vector2 LocalPosition)> _detachEnts = new();
|
||||
|
||||
private void InitializeMagnet()
|
||||
{
|
||||
_salvMobQuery = GetEntityQuery<SalvageMobRestrictionsComponent>();
|
||||
_mobStateQuery = GetEntityQuery<MobStateComponent>();
|
||||
|
||||
SubscribeLocalEvent<SalvageMagnetDataComponent, MapInitEvent>(OnMagnetDataMapInit);
|
||||
|
||||
@@ -155,6 +157,21 @@ public sealed partial class SalvageSystem
|
||||
if (_salvMobQuery.HasComp(mobUid))
|
||||
continue;
|
||||
|
||||
bool CheckParents(EntityUid uid)
|
||||
{
|
||||
do
|
||||
{
|
||||
uid = _transform.GetParentUid(uid);
|
||||
if (_mobStateQuery.HasComp(uid))
|
||||
return true;
|
||||
}
|
||||
while (uid != xform.GridUid && uid != EntityUid.Invalid);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CheckParents(mobUid))
|
||||
continue;
|
||||
|
||||
// Can't parent directly to map as it runs grid traversal.
|
||||
_detachEnts.Add(((mobUid, xform), xform.MapUid.Value, _transform.GetWorldPosition(xform)));
|
||||
_transform.DetachEntity(mobUid, xform);
|
||||
|
||||
@@ -231,7 +231,7 @@ public sealed class IonStormSystem : EntitySystem
|
||||
return _robustRandom.Next(0, 35) switch
|
||||
{
|
||||
0 => Loc.GetString("ion-storm-law-on-station", ("joined", joined), ("subjects", triple)),
|
||||
1 => Loc.GetString("ion-storm-law-no-shuttle", ("joined", joined), ("subjects", triple)),
|
||||
1 => Loc.GetString("ion-storm-law-call-shuttle", ("joined", joined), ("subjects", triple)),
|
||||
2 => Loc.GetString("ion-storm-law-crew-are", ("who", crewAll), ("joined", joined), ("subjects", objectsThreats)),
|
||||
3 => Loc.GetString("ion-storm-law-subjects-harmful", ("adjective", adjective), ("subjects", triple)),
|
||||
4 => Loc.GetString("ion-storm-law-must-harmful", ("must", must)),
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using System.Text;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class DamagedSiliconAccentSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DamagedSiliconAccentComponent, AccentGetEvent>(OnAccent, after: [typeof(ReplacementAccentSystem)]);
|
||||
}
|
||||
|
||||
private void OnAccent(Entity<DamagedSiliconAccentComponent> ent, ref AccentGetEvent args)
|
||||
{
|
||||
var uid = ent.Owner;
|
||||
|
||||
if (ent.Comp.EnableChargeCorruption)
|
||||
{
|
||||
var currentChargeLevel = 0.0f;
|
||||
if (ent.Comp.OverrideChargeLevel.HasValue)
|
||||
{
|
||||
currentChargeLevel = ent.Comp.OverrideChargeLevel.Value;
|
||||
}
|
||||
else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
|
||||
{
|
||||
currentChargeLevel = battery.CurrentCharge / battery.MaxCharge;
|
||||
}
|
||||
currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f);
|
||||
// Corrupt due to low power (drops characters on longer messages)
|
||||
args.Message = CorruptPower(args.Message, currentChargeLevel, ref ent.Comp);
|
||||
}
|
||||
|
||||
if (ent.Comp.EnableDamageCorruption)
|
||||
{
|
||||
var damage = FixedPoint2.Zero;
|
||||
if (ent.Comp.OverrideTotalDamage.HasValue)
|
||||
{
|
||||
damage = ent.Comp.OverrideTotalDamage.Value;
|
||||
}
|
||||
else if (TryComp<DamageableComponent>(uid, out var damageable))
|
||||
{
|
||||
damage = damageable.TotalDamage;
|
||||
}
|
||||
// Corrupt due to damage (drop, repeat, replace with symbols)
|
||||
args.Message = CorruptDamage(args.Message, damage, ref ent.Comp);
|
||||
}
|
||||
}
|
||||
|
||||
public string CorruptPower(string message, float chargeLevel, ref DamagedSiliconAccentComponent comp)
|
||||
{
|
||||
// The first idxMin characters are SAFE
|
||||
var idxMin = comp.StartPowerCorruptionAtCharIdx;
|
||||
// Probability will max at idxMax
|
||||
var idxMax = comp.MaxPowerCorruptionAtCharIdx;
|
||||
|
||||
// Fast bails, would not have an effect
|
||||
if (chargeLevel > comp.ChargeThresholdForPowerCorruption || message.Length < idxMin)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
var outMsg = new StringBuilder();
|
||||
|
||||
var maxDropProb = comp.MaxDropProbFromPower * (1.0f - chargeLevel / comp.ChargeThresholdForPowerCorruption);
|
||||
|
||||
var idx = -1;
|
||||
foreach (var letter in message)
|
||||
{
|
||||
idx++;
|
||||
if (idx < idxMin) // Fast character, no effect
|
||||
{
|
||||
outMsg.Append(letter);
|
||||
continue;
|
||||
}
|
||||
|
||||
// use an x^2 interpolation to increase the drop probability until we hit idxMax
|
||||
var probToDrop = idx >= idxMax
|
||||
? maxDropProb
|
||||
: (float)Math.Pow(((double)idx - idxMin) / (idxMax - idxMin), 2.0) * maxDropProb;
|
||||
// Ensure we're in the range for Prob()
|
||||
probToDrop = Math.Clamp(probToDrop, 0.0f, 1.0f);
|
||||
|
||||
if (_random.Prob(probToDrop)) // Lose a character
|
||||
{
|
||||
// Additional chance to change to dot for flavor instead of full drop
|
||||
if (_random.Prob(comp.ProbToCorruptDotFromPower))
|
||||
{
|
||||
outMsg.Append('.');
|
||||
}
|
||||
}
|
||||
else // Character is safe
|
||||
{
|
||||
outMsg.Append(letter);
|
||||
}
|
||||
}
|
||||
return outMsg.ToString();
|
||||
}
|
||||
|
||||
private string CorruptDamage(string message, FixedPoint2 totalDamage, ref DamagedSiliconAccentComponent comp)
|
||||
{
|
||||
var outMsg = new StringBuilder();
|
||||
// Linear interpolation of character damage probability
|
||||
var damagePercent = Math.Clamp((float)totalDamage / (float)comp.DamageAtMaxCorruption, 0, 1);
|
||||
var chanceToCorruptLetter = damagePercent * comp.MaxDamageCorruption;
|
||||
foreach (var letter in message)
|
||||
{
|
||||
if (_random.Prob(chanceToCorruptLetter)) // Corrupt!
|
||||
{
|
||||
outMsg.Append(CorruptLetterDamage(letter));
|
||||
}
|
||||
else // Safe!
|
||||
{
|
||||
outMsg.Append(letter);
|
||||
}
|
||||
}
|
||||
return outMsg.ToString();
|
||||
}
|
||||
|
||||
private string CorruptLetterDamage(char letter)
|
||||
{
|
||||
var res = _random.NextDouble();
|
||||
return res switch
|
||||
{
|
||||
< 0.0 => letter.ToString(), // shouldn't be less than 0!
|
||||
< 0.5 => CorruptPunctuize(), // 50% chance to replace with random punctuation
|
||||
< 0.75 => "", // 25% chance to remove character
|
||||
< 1.00 => CorruptRepeat(letter), // 25% to repeat the character
|
||||
_ => letter.ToString(), // shouldn't be greater than 1!
|
||||
};
|
||||
}
|
||||
|
||||
private string CorruptPunctuize()
|
||||
{
|
||||
const string punctuation = "\"\\`~!@#$%^&*()_+-={}[]|\\;:<>,.?/";
|
||||
return punctuation[_random.NextByte((byte)punctuation.Length)].ToString();
|
||||
}
|
||||
|
||||
private string CorruptRepeat(char letter)
|
||||
{
|
||||
// 25% chance to add another character in the streak
|
||||
// (kind of like "exploding dice")
|
||||
// Solved numerically in closed form for streaks of bernoulli variables with p = 0.25
|
||||
// Can calculate for different p using python function:
|
||||
/*
|
||||
* def prob(streak, p):
|
||||
* if streak == 0:
|
||||
* return scipy.stats.binom(streak+1, p).pmf(streak)
|
||||
* return prob(streak-1) * p
|
||||
* def prob_cum(streak, p=.25):
|
||||
* return np.sum([prob(i, p) for i in range(streak+1)])
|
||||
*/
|
||||
var numRepeats = _random.NextDouble() switch
|
||||
{
|
||||
< 0.75000000 => 2,
|
||||
< 0.93750000 => 3,
|
||||
< 0.98437500 => 4,
|
||||
< 0.99609375 => 5,
|
||||
< 0.99902344 => 6,
|
||||
< 0.99975586 => 7,
|
||||
< 0.99993896 => 8,
|
||||
< 0.99998474 => 9,
|
||||
_ => 10,
|
||||
};
|
||||
return new string(letter, numRepeats);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Events;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
@@ -17,6 +19,8 @@ public sealed partial class StationJobsSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IBanManager _banManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
|
||||
private Dictionary<int, HashSet<string>> _jobsByWeight = default!;
|
||||
private List<int> _orderedWeights = default!;
|
||||
@@ -345,6 +349,7 @@ public sealed partial class StationJobsSystem
|
||||
foreach (var (player, profile) in profiles)
|
||||
{
|
||||
var roleBans = _banManager.GetJobBans(player);
|
||||
var antagBlocked = _antag.GetPreSelectedAntagSessions();
|
||||
var profileJobs = profile.JobPriorities.Keys.Select(k => new ProtoId<JobPrototype>(k)).ToList();
|
||||
var ev = new StationJobsGetCandidatesEvent(player, profileJobs);
|
||||
RaiseLocalEvent(ref ev);
|
||||
@@ -361,6 +366,9 @@ public sealed partial class StationJobsSystem
|
||||
if (!_prototypeManager.TryIndex(jobId, out var job))
|
||||
continue;
|
||||
|
||||
if (!job.CanBeAntag && (!_playerManager.TryGetSessionById(player, out var session) || antagBlocked.Contains(session)))
|
||||
continue;
|
||||
|
||||
if (weight is not null && job.Weight != weight.Value)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
// Unfortunately this means that an event is called for it as well, and since TryFindIdCard will succeed if the
|
||||
// given entity is a card and the card itself is the key the record will be mistakenly renamed to the card's name
|
||||
// if we don't return early.
|
||||
if (HasComp<IdCardComponent>(ev.Uid))
|
||||
// We also do not include the PDA itself being renamed, as that triggers the same event (e.g. for chameleon PDAs).
|
||||
if (HasComp<IdCardComponent>(ev.Uid) || HasComp<PdaComponent>(ev.Uid))
|
||||
return;
|
||||
|
||||
if (_idCard.TryFindIdCard(ev.Uid, out var idCard))
|
||||
|
||||
@@ -449,9 +449,14 @@ public enum LogType
|
||||
/// An atmos networked device (such as a vent or pump) has had its settings changed, usually through an air alarm
|
||||
/// </summary>
|
||||
AtmosDeviceSetting = 97,
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Commands related to admemes. Stuff like config changes, etc.
|
||||
/// </summary>
|
||||
AdminCommands = 98,
|
||||
|
||||
/// <summary>
|
||||
/// A player was selected or assigned antag status
|
||||
/// </summary>
|
||||
AntagSelection = 99,
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ public abstract class SharedAnomalySystem : EntitySystem
|
||||
var ev = new AnomalySupercriticalEvent(uid, powerMod);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
|
||||
EndAnomaly(uid, component, true);
|
||||
EndAnomaly(uid, component, true, logged: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -173,13 +173,17 @@ public abstract class SharedAnomalySystem : EntitySystem
|
||||
/// <param name="component"></param>
|
||||
/// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
|
||||
/// <param name="spawnCore">Create anomaly cores based on the result of completing an anomaly?</param>
|
||||
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true)
|
||||
/// <param name="logged">Whether or not the anomaly decaying/going supercritical is logged</param>
|
||||
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false)
|
||||
{
|
||||
// Logging before resolve, in case the anomaly has deleted itself.
|
||||
if (_net.IsServer)
|
||||
Log.Info($"Ending anomaly. Entity: {ToPrettyString(uid)}");
|
||||
AdminLog.Add(LogType.Anomaly, supercritical ? LogImpact.High : LogImpact.Low,
|
||||
$"Anomaly {ToPrettyString(uid)} {(supercritical ? "went supercritical" : "decayed")}.");
|
||||
if (logged)
|
||||
{
|
||||
// Logging before resolve, in case the anomaly has deleted itself.
|
||||
if (_net.IsServer)
|
||||
Log.Info($"Ending anomaly. Entity: {ToPrettyString(uid)}");
|
||||
AdminLog.Add(LogType.Anomaly, supercritical ? LogImpact.High : LogImpact.Low,
|
||||
$"Anomaly {ToPrettyString(uid)} {(supercritical ? "went supercritical" : "decayed")}.");
|
||||
}
|
||||
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
@@ -260,7 +264,7 @@ public abstract class SharedAnomalySystem : EntitySystem
|
||||
|
||||
if (newVal < 0)
|
||||
{
|
||||
EndAnomaly(uid, component);
|
||||
EndAnomaly(uid, component, logged: true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public enum AntagAcceptability
|
||||
/// <summary>
|
||||
/// Choose anyone
|
||||
/// </summary>
|
||||
All
|
||||
All,
|
||||
}
|
||||
|
||||
public enum AntagSelectionTime : byte
|
||||
@@ -28,8 +28,14 @@ public enum AntagSelectionTime : byte
|
||||
/// </summary>
|
||||
PrePlayerSpawn,
|
||||
|
||||
/// <summary>
|
||||
/// Antag roles are selected to the player session before job assignment and spawning.
|
||||
/// Unlike PrePlayerSpawn, this does not remove you from the job spawn pool.
|
||||
/// </summary>
|
||||
IntraPlayerSpawn,
|
||||
|
||||
/// <summary>
|
||||
/// Antag roles get assigned after players have been assigned jobs and have spawned in.
|
||||
/// </summary>
|
||||
PostPlayerSpawn
|
||||
PostPlayerSpawn,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,12 @@ public sealed partial class ArmorComponent : Component
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float PriceMultiplier = 1;
|
||||
|
||||
/// <summary>
|
||||
/// If true, you can examine the armor to see the protection. If false, the verb won't appear.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool ShowArmorOnExamine = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -51,7 +51,7 @@ public abstract class SharedArmorSystem : EntitySystem
|
||||
|
||||
private void OnArmorVerbExamine(EntityUid uid, ArmorComponent component, GetVerbsEvent<ExamineVerb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess)
|
||||
if (!args.CanInteract || !args.CanAccess || !component.ShowArmorOnExamine)
|
||||
return;
|
||||
|
||||
var examineMarkup = GetArmorExamine(component.Modifiers);
|
||||
|
||||
@@ -416,7 +416,7 @@ public abstract partial class SharedBuckleSystem
|
||||
|
||||
public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
|
||||
{
|
||||
if (!Resolve(buckle.Owner, ref buckle.Comp))
|
||||
if (!Resolve(buckle.Owner, ref buckle.Comp, false))
|
||||
return false;
|
||||
|
||||
if (!CanUnbuckle(buckle, user, popup, out var strap))
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Component that indicates a PDA cartridge as containing the NanoTask program
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class NanoTaskCartridgeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of tasks
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<NanoTaskItemAndId> Tasks = new();
|
||||
|
||||
/// <summary>
|
||||
/// counter for generating task IDs
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Counter = 1;
|
||||
|
||||
/// <summary>
|
||||
/// When the user can print again
|
||||
/// </summary>
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan NextPrintAllowedAfter = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// How long in between each time the user can print out a task
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan PrintDelay = TimeSpan.FromSeconds(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Component attached to the PDA a NanoTask cartridge is inserted into for interaction handling
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class NanoTaskInteractionComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Component attached to a piece of paper to indicate that it was printed from NanoTask and can be inserted back into it
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class NanoTaskPrintedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The task that this item holds
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public NanoTaskItem? Task;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// Base UI message for NanoTask interactions
|
||||
/// </summary>
|
||||
public interface INanoTaskUiMessagePayload
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatched when a new task is created
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public sealed class NanoTaskAddTask : INanoTaskUiMessagePayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The newly created task
|
||||
/// </summary>
|
||||
public readonly NanoTaskItem Item;
|
||||
|
||||
public NanoTaskAddTask(NanoTaskItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatched when an existing task is modified
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public sealed class NanoTaskUpdateTask : INanoTaskUiMessagePayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The task that was updated and its ID
|
||||
/// </summary>
|
||||
public readonly NanoTaskItemAndId Item;
|
||||
|
||||
public NanoTaskUpdateTask(NanoTaskItemAndId item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatched when an existing task is deleted
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public sealed class NanoTaskDeleteTask : INanoTaskUiMessagePayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the task to delete
|
||||
/// </summary>
|
||||
public readonly int Id;
|
||||
|
||||
public NanoTaskDeleteTask(int id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatched when a task is requested to be printed
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public sealed class NanoTaskPrintTask : INanoTaskUiMessagePayload
|
||||
{
|
||||
/// <summary>
|
||||
/// The NanoTask to print
|
||||
/// </summary>
|
||||
public readonly NanoTaskItem Item;
|
||||
|
||||
public NanoTaskPrintTask(NanoTaskItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cartridge message event carrying the NanoTask UI messages
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class NanoTaskUiMessageEvent : CartridgeMessageEvent
|
||||
{
|
||||
public readonly INanoTaskUiMessagePayload Payload;
|
||||
public NanoTaskUiMessageEvent(INanoTaskUiMessagePayload payload)
|
||||
{
|
||||
Payload = payload;
|
||||
}
|
||||
}
|
||||
88
Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiState.cs
Normal file
88
Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiState.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
/// <summary>
|
||||
/// The priority assigned to a NanoTask item
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum NanoTaskPriority : byte
|
||||
{
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The data relating to a single NanoTask item, but not its identifier
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public sealed class NanoTaskItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum length of the Description and TaskIsFor fields
|
||||
/// </summary>
|
||||
public static int MaximumStringLength = 30;
|
||||
|
||||
/// <summary>
|
||||
/// The task description, i.e. "Bake a cake"
|
||||
/// </summary>
|
||||
public readonly string Description;
|
||||
|
||||
/// <summary>
|
||||
/// Who the task is for, i.e. "Cargo"
|
||||
/// </summary>
|
||||
public readonly string TaskIsFor;
|
||||
|
||||
/// <summary>
|
||||
/// If the task is marked as done or not
|
||||
/// </summary>
|
||||
public readonly bool IsTaskDone;
|
||||
|
||||
/// <summary>
|
||||
/// The task's marked priority
|
||||
/// </summary>
|
||||
public readonly NanoTaskPriority Priority;
|
||||
|
||||
public NanoTaskItem(string description, string taskIsFor, bool isTaskDone, NanoTaskPriority priority)
|
||||
{
|
||||
Description = description;
|
||||
TaskIsFor = taskIsFor;
|
||||
IsTaskDone = isTaskDone;
|
||||
Priority = priority;
|
||||
}
|
||||
public bool Validate()
|
||||
{
|
||||
return Description.Length <= MaximumStringLength && TaskIsFor.Length <= MaximumStringLength;
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Pairs a NanoTask item and its identifier
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable, DataRecord]
|
||||
public sealed class NanoTaskItemAndId
|
||||
{
|
||||
public readonly int Id;
|
||||
public readonly NanoTaskItem Data;
|
||||
|
||||
public NanoTaskItemAndId(int id, NanoTaskItem data)
|
||||
{
|
||||
Id = id;
|
||||
Data = data;
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The UI state of the NanoTask
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class NanoTaskUiState : BoundUserInterfaceState
|
||||
{
|
||||
public List<NanoTaskItemAndId> Tasks;
|
||||
|
||||
public NanoTaskUiState(List<NanoTaskItemAndId> tasks)
|
||||
{
|
||||
Tasks = tasks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
namespace Content.Shared.CartridgeLoader.Cartridges;
|
||||
|
||||
public abstract class SharedNanoTaskCartridgeSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded);
|
||||
}
|
||||
|
||||
private void OnCartridgeAdded(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeAddedEvent args)
|
||||
{
|
||||
EnsureComp<NanoTaskInteractionComponent>(args.Loader);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,19 @@ public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPr
|
||||
/// Disabled when null.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
|
||||
public SlotFlags? CopyEquipment = SlotFlags.All;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to copy slime storage and storage implant contents.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool CopyInternalStorage = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to copy implants.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool CopyImplants = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist for the equipment allowed to be copied.
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed partial class CriminalRecordsHackerComponent : Component
|
||||
/// Dataset of random reasons to use.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<DatasetPrototype> Reasons = "CriminalRecordsWantedReasonPlaceholders";
|
||||
public ProtoId<LocalizedDatasetPrototype> Reasons = "CriminalRecordsWantedReasonPlaceholders";
|
||||
|
||||
/// <summary>
|
||||
/// Announcement made after the console is hacked.
|
||||
|
||||
@@ -82,7 +82,7 @@ public sealed partial class ElectrifiedComponent : Component
|
||||
/// Shock time multiplier for HV electrocution
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float HighVoltageTimeMultiplier = 1.5f;
|
||||
public float HighVoltageTimeMultiplier = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// Damage multiplier for MV electrocution
|
||||
@@ -94,7 +94,7 @@ public sealed partial class ElectrifiedComponent : Component
|
||||
/// Shock time multiplier for MV electrocution
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float MediumVoltageTimeMultiplier = 1.25f;
|
||||
public float MediumVoltageTimeMultiplier = 1.5f;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public float ShockDamage = 7.5f;
|
||||
@@ -103,7 +103,7 @@ public sealed partial class ElectrifiedComponent : Component
|
||||
/// Shock time, in seconds.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float ShockTime = 8f;
|
||||
public float ShockTime = 5f;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public float SiemensCoefficient = 1f;
|
||||
|
||||
22
Content.Shared/Gibbing/Components/GibOnRoundEndComponent.cs
Normal file
22
Content.Shared/Gibbing/Components/GibOnRoundEndComponent.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Gibbing.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Gibs an entity on round end.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class GibOnRoundEndComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// If the entity has all these objectives fulfilled they won't be gibbed.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<EntProtoId> PreventGibbingObjectives = new();
|
||||
|
||||
/// <summary>
|
||||
/// Entity to spawn when gibbed. Can be used for effects.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? SpawnProto;
|
||||
}
|
||||
@@ -49,11 +49,17 @@ public static class Identity
|
||||
/// This is an extension method because of its simplicity, and if it was any harder to call it might not
|
||||
/// be used enough for loc.
|
||||
/// </summary>
|
||||
public static EntityUid Entity(EntityUid uid, IEntityManager ent)
|
||||
/// <param name="viewer">
|
||||
/// If this entity can see through identities, this method will always return the actual target entity.
|
||||
/// </param>
|
||||
public static EntityUid Entity(EntityUid uid, IEntityManager ent, EntityUid? viewer = null)
|
||||
{
|
||||
if (!ent.TryGetComponent<IdentityComponent>(uid, out var identity))
|
||||
return uid;
|
||||
|
||||
if (viewer != null && CanSeeThroughIdentity(uid, viewer.Value, ent))
|
||||
return uid;
|
||||
|
||||
return identity.IdentityEntitySlot.ContainedEntity ?? uid;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
@@ -16,7 +16,7 @@ public sealed partial class CryoPodComponent : Component
|
||||
public string PortName { get; set; } = "port";
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the name of the atmospherics port to draw gas from.
|
||||
/// Specifies the name of the slot that holds beaker with medicine.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("solutionContainerName")]
|
||||
|
||||
@@ -11,6 +11,7 @@ using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
@@ -26,6 +27,7 @@ public abstract class SharedMindSystem : EntitySystem
|
||||
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||
[Dependency] private readonly SharedPlayerSystem _player = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
|
||||
[ViewVariables]
|
||||
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
|
||||
@@ -364,6 +366,16 @@ public abstract class SharedMindSystem : EntitySystem
|
||||
var title = Name(objective);
|
||||
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}");
|
||||
mind.Objectives.Remove(objective);
|
||||
|
||||
// garbage collection - only delete the objective entity if no mind uses it anymore
|
||||
// This comes up for stuff like paradox clones where the objectives share the same entity
|
||||
var mindQuery = new AllEntityQueryEnumerator<MindComponent>();
|
||||
while (mindQuery.MoveNext(out _, out var queryComp))
|
||||
{
|
||||
if (queryComp.Objectives.Contains(objective))
|
||||
return true;
|
||||
}
|
||||
|
||||
Del(objective);
|
||||
return true;
|
||||
}
|
||||
@@ -395,6 +407,33 @@ public abstract class SharedMindSystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies objectives from one mind to another, so that they are shared between two players.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only copies the reference to the objective entity, not the entity itself.
|
||||
/// This relies on the fact that objectives are never changed after spawning them.
|
||||
/// If someone ever changes that, they will have to address this.
|
||||
/// </remarks>
|
||||
/// <param name="source"> mind entity of the player to copy from </param>
|
||||
/// <param name="target"> mind entity of the player to copy to </param>
|
||||
/// <param name="except"> whitelist for objectives that should be copied </param>
|
||||
/// <param name="except"> blacklist for objectives that should not be copied </param>
|
||||
public void CopyObjectives(Entity<MindComponent?> source, Entity<MindComponent?> target, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||
{
|
||||
if (!Resolve(source, ref source.Comp) || !Resolve(target, ref target.Comp))
|
||||
return;
|
||||
|
||||
foreach (var objective in source.Comp.Objectives)
|
||||
{
|
||||
if (target.Comp.Objectives.Contains(objective))
|
||||
continue; // target already has this objective
|
||||
|
||||
if (_whitelist.CheckBoth(objective, blacklist, whitelist))
|
||||
AddObjective(target, target.Comp, objective);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find an objective that has the same prototype as the argument.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using Content.Shared.Revolutionary;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Mindshield.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Component given to an entity to mark it is a mindshield implant.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedRevolutionarySystem))]
|
||||
public sealed partial class MindShieldImplantComponent : Component;
|
||||
@@ -128,6 +128,18 @@ public sealed partial record PolymorphConfiguration
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? ExitPolymorphSound;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this popup will be displayed when being polymorphed into something.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? PolymorphPopup = "polymorph-popup-generic";
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this popup will be displayed when when being reverted from a polymorph.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? ExitPolymorphPopup = "polymorph-revert-popup-generic";
|
||||
}
|
||||
|
||||
public enum PolymorphInventoryChange : byte
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Speech.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
[NetworkedComponent]
|
||||
public sealed partial class DamagedSiliconAccentComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable damage corruption effects
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool EnableDamageCorruption = true;
|
||||
|
||||
/// <summary>
|
||||
/// Override total damage for damage corruption effects
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2? OverrideTotalDamage;
|
||||
|
||||
/// <summary>
|
||||
/// The probability that a character will be corrupted when total damage at or above <see cref="MaxDamageCorruption" />.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxDamageCorruption = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Probability of character corruption will increase linearly to <see cref="MaxDamageCorruption" /> once until
|
||||
/// total damage is at or above this value.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2 DamageAtMaxCorruption = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Enable charge level corruption effects
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool EnableChargeCorruption = true;
|
||||
|
||||
/// <summary>
|
||||
/// Override charge level for charge level corruption effects
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float? OverrideChargeLevel;
|
||||
|
||||
/// <summary>
|
||||
/// If the power cell charge is below this value (as a fraction of maximum charge),
|
||||
/// power corruption will begin to be applied.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ChargeThresholdForPowerCorruption = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// Regardless of charge level, this is how many characters at the start of a message will be 100% safe
|
||||
/// from being dropped.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int StartPowerCorruptionAtCharIdx = 8;
|
||||
|
||||
/// <summary>
|
||||
/// The probability that a character will be dropped due to charge level will be maximum for characters past
|
||||
/// this index. This has the effect of longer messages dropping more characters later in the message.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxPowerCorruptionAtCharIdx = 40;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum probability that a character will be dropped due to charge level.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxDropProbFromPower = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// If a character is "dropped", this is the probability that the character will be turned into a period instead
|
||||
/// of completely deleting the character.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float ProbToCorruptDotFromPower = 0.6f;
|
||||
}
|
||||
@@ -145,6 +145,12 @@ namespace Content.Shared.Throwing
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// TODO predicted throwing - remove this check
|
||||
// We don't want to predict landing or stopping, since throwing isn't actually predicted.
|
||||
// If we do, the landing/stop will occur prematurely on the client.
|
||||
if (_gameTiming.InPrediction)
|
||||
return;
|
||||
|
||||
var query = EntityQueryEnumerator<ThrownItemComponent, PhysicsComponent>();
|
||||
while (query.MoveNext(out var uid, out var thrown, out var physics))
|
||||
{
|
||||
|
||||
@@ -350,10 +350,7 @@ public abstract partial class SharedGunSystem : EntitySystem
|
||||
// If they're firing an existing clip then don't play anything.
|
||||
if (shots > 0)
|
||||
{
|
||||
if (ev.Reason != null && Timing.IsFirstTimePredicted)
|
||||
{
|
||||
PopupSystem.PopupCursor(ev.Reason);
|
||||
}
|
||||
PopupSystem.PopupCursor(ev.Reason ?? Loc.GetString("gun-magazine-fired-empty"));
|
||||
|
||||
// Don't spam safety sounds at gun fire rate, play it at a reduced rate.
|
||||
// May cause prediction issues? Needs more tweaking
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
license: "Custom"
|
||||
copyright: "Space Asshole by Chris Remo is used with special permission from the author, under the condition that the project remains non-commercial and open source. The author also requested that a link to his bandcamp be included: https://chrisremo.bandcamp.com/"
|
||||
source: "https://idlethumbs.bandcamp.com/track/space-asshole"
|
||||
# The source is a direct link the the track, but not the "main" bandcamp of the author. Hence the link is also included separately in the copyright.
|
||||
# The source is a direct link to the track, but not the "main" bandcamp of the author. Hence the link is also included separately in the copyright.
|
||||
|
||||
- files: ["the_wizard.ogg"]
|
||||
license: "Custom"
|
||||
@@ -49,7 +49,7 @@
|
||||
- files: ["comet_haley.ogg"]
|
||||
license: "CC-BY-NC-SA-3.0"
|
||||
copyright: "Comet Haley by Stellardrone. Converted from MP3 to OGG."
|
||||
source: "https://freemusicarchive.org/music/Stellardrone/Light_Years_1227/07_Comet_Halley"
|
||||
source: "https://stellardrone.bandcamp.com/track/comet-halley"
|
||||
|
||||
- files: ["mod.flip-flap.ogg"]
|
||||
license: "Custom"
|
||||
@@ -63,15 +63,15 @@
|
||||
|
||||
- files: ["pwmur.ogg"]
|
||||
license: "CC-BY-NC-SA-3.0"
|
||||
copyright: "phoron will make us rich by Alexander Divine."
|
||||
source: "https://soundcloud.com/alexanderdivine/phoron-will-make-us-rich"
|
||||
copyright: "phoron will make us rich by Sunbeamstress/Lauren Loveless."
|
||||
source: "https://soundcloud.com/sunbeamstress/phoron-will-make-us-rich"
|
||||
|
||||
- files: ["lasers_rip_apart_the_bulkhead.ogg"]
|
||||
license: "CC-BY-NC-SA-3.0"
|
||||
copyright: "lasers rip apart the bulkhead by Alexander Divine."
|
||||
source: "https://soundcloud.com/alexanderdivine/lasers-rip-apart-the-bulkhead"
|
||||
copyright: "lasers rip apart the bulkhead by Sunbeamstress/Lauren Loveless."
|
||||
source: "https://soundcloud.com/sunbeamstress/lasers-rip-apart-the-bulkhead"
|
||||
|
||||
- files: ["every_light_is_blinking_at_once.ogg"]
|
||||
license: "CC-BY-NC-SA-3.0"
|
||||
copyright: "every light is blinking at once by Alexander Divine."
|
||||
source: "https://soundcloud.com/alexanderdivine/every-light-is-blinking-at-once"
|
||||
copyright: "every light is blinking at once by Sunbeamstress/Lauren Loveless."
|
||||
source: "https://soundcloud.com/sunbeamstress/every-light-is-blinking-at-once"
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
copyright: "Created by Chillyconmor"
|
||||
source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Audio/Misc/ninja_greeting.ogg"
|
||||
|
||||
- files: ["nparadox_clone_greeting.ogg"]
|
||||
license: "CC-BY-SA-3.0"
|
||||
copyright: "Created by ps3moira"
|
||||
source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Audio/Misc/ninja_greeting.ogg"
|
||||
|
||||
- files: ["thief_greeting.ogg"]
|
||||
license: "CC-BY-NC-4.0"
|
||||
copyright: "Taken from SergeQuadrado via freesound.org, edit and mono by TheShuEd"
|
||||
|
||||
BIN
Resources/Audio/Misc/paradox_clone_greeting.ogg
Normal file
BIN
Resources/Audio/Misc/paradox_clone_greeting.ogg
Normal file
Binary file not shown.
@@ -872,5 +872,22 @@ Entries:
|
||||
id: 108
|
||||
time: '2025-03-10T09:40:36.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35747
|
||||
- author: Errant
|
||||
changes:
|
||||
- message: The admin overlay once again shows the character name in the top line.
|
||||
type: Fix
|
||||
id: 109
|
||||
time: '2025-03-11T16:47:03.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35783
|
||||
- author: Errant
|
||||
changes:
|
||||
- message: All Solo Antagonist, Team Antagonist, Altered Silicon and (non-harmless)
|
||||
Free Agent ghostroles are now marked as Antagonist on all interfaces where this
|
||||
distinction exists. (Classic admin overlay, admin playerlist, round end player
|
||||
list)
|
||||
type: Fix
|
||||
id: 110
|
||||
time: '2025-03-16T22:11:31.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35832
|
||||
Name: Admin
|
||||
Order: 1
|
||||
|
||||
@@ -1,258 +1,4 @@
|
||||
Entries:
|
||||
- author: BramvanZijp
|
||||
changes:
|
||||
- message: The Space Ninja Suit will now give an error popup if you are trying to
|
||||
install a cell that is not better compared to the current cell.
|
||||
type: Add
|
||||
- message: When comparing which power cell is better when trying to swap them, the
|
||||
Space Ninja's Suit will now also consider if the power cells have self-recharge
|
||||
capability.
|
||||
type: Tweak
|
||||
- message: You can no longer fit weapons-grade power cages into the space ninja
|
||||
suit.
|
||||
type: Fix
|
||||
id: 7548
|
||||
time: '2024-10-22T23:36:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32902
|
||||
- author: UBlueberry
|
||||
changes:
|
||||
- message: The appraisal tool now has in-hand sprites!
|
||||
type: Fix
|
||||
id: 7549
|
||||
time: '2024-10-22T23:51:49.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32849
|
||||
- author: chromiumboy
|
||||
changes:
|
||||
- message: The atmospheric alerts computer has been upgraded to visually indicate
|
||||
the rooms of the station that are being monitored by its air and fire alarm
|
||||
systems
|
||||
type: Tweak
|
||||
id: 7550
|
||||
time: '2024-10-23T12:49:58.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/31910
|
||||
- author: hyphenationc
|
||||
changes:
|
||||
- message: Snake meat is now properly considered Meat, so Lizards can eat it.
|
||||
type: Fix
|
||||
id: 7551
|
||||
time: '2024-10-24T03:41:03.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32965
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Mix 1u aluminium, 1u potassium and 1u sulfur for a flash reaction effect.
|
||||
The radius scales with the reagent amount.
|
||||
type: Add
|
||||
id: 7552
|
||||
time: '2024-10-25T22:47:12.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32377
|
||||
- author: BramvanZijp
|
||||
changes:
|
||||
- message: Fixed the Lone Nuclear Operative mid-round antagonist being extremely
|
||||
rare.
|
||||
type: Fix
|
||||
id: 7553
|
||||
time: '2024-10-26T02:16:45.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32942
|
||||
- author: Moomoobeef
|
||||
changes:
|
||||
- message: Bowls no longer make an eating sound when drinking from them.
|
||||
type: Fix
|
||||
id: 7554
|
||||
time: '2024-10-26T04:00:49.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32819
|
||||
- author: SaphireLattice
|
||||
changes:
|
||||
- message: Added a warning about unrevivability in the health analyzer UI.
|
||||
type: Add
|
||||
id: 7555
|
||||
time: '2024-10-26T17:22:09.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32636
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Fixed pie throwing sound not playing.
|
||||
type: Fix
|
||||
id: 7556
|
||||
time: '2024-10-27T04:25:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33017
|
||||
- author: stalengd
|
||||
changes:
|
||||
- message: Fixed playtime labels not being able to correctly display time greater
|
||||
than 24 hours
|
||||
type: Fix
|
||||
id: 7557
|
||||
time: '2024-10-28T18:00:00.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32974
|
||||
- author: august-sun
|
||||
changes:
|
||||
- message: Extended the minimum round time for meteor swarm events.
|
||||
type: Tweak
|
||||
id: 7558
|
||||
time: '2024-10-28T21:25:34.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32876
|
||||
- author: deltanedas
|
||||
changes:
|
||||
- message: Fixed lava planet expeditions not working.
|
||||
type: Fix
|
||||
id: 7559
|
||||
time: '2024-10-29T05:00:29.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33042
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- message: Fix separated game screen bumping slightly.
|
||||
type: Fix
|
||||
id: 7560
|
||||
time: '2024-10-29T05:07:57.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33046
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Proto-kitentic crushers, glaives, and daggers now have more accurate
|
||||
inhand sprites.
|
||||
type: Tweak
|
||||
id: 7561
|
||||
time: '2024-10-30T07:38:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32212
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Security belts now contain a holobarrier projector and a handheld security
|
||||
radio by default rather than tear gas and a flashbang.
|
||||
type: Tweak
|
||||
id: 7562
|
||||
time: '2024-10-30T07:40:33.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32291
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Added three bottle boxes to the nanomed plus inventory for doctors to
|
||||
carry small amounts of chemicals on their person
|
||||
type: Add
|
||||
id: 7563
|
||||
time: '2024-10-30T07:41:51.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33018
|
||||
- author: Blackern5000
|
||||
changes:
|
||||
- message: Added the interdyne defibrillator, a black-and-red defibrillator that
|
||||
can be used as a melee weapon.
|
||||
type: Add
|
||||
- message: The syndicate medical bundle now contains an interdyne defibrillator,
|
||||
a collection of various instant injectors, tourniquets, and several combat kits.
|
||||
The price has been raised to 24 tc.
|
||||
type: Tweak
|
||||
id: 7564
|
||||
time: '2024-10-30T09:15:30.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32720
|
||||
- author: Boaz1111
|
||||
changes:
|
||||
- message: Pill bottles can now only store pills.
|
||||
type: Tweak
|
||||
id: 7565
|
||||
time: '2024-10-31T10:56:07.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33074
|
||||
- author: Jarmer123
|
||||
changes:
|
||||
- message: You can now find a spare bible in the PietyVend
|
||||
type: Add
|
||||
id: 7566
|
||||
time: '2024-10-31T13:26:46.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32363
|
||||
- author: justinbrick
|
||||
changes:
|
||||
- message: Added a pop-up notification when extra items are dropped while unequipping
|
||||
something.
|
||||
type: Tweak
|
||||
id: 7567
|
||||
time: '2024-10-31T14:12:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33078
|
||||
- author: BramvanZijp
|
||||
changes:
|
||||
- message: The maximum amount of programs that can be installed on a PDA has been
|
||||
increased from 5 to 8
|
||||
type: Tweak
|
||||
- message: The Detective and Head of Security now get the logprobe program pre-installed
|
||||
on their PDA.
|
||||
type: Tweak
|
||||
id: 7568
|
||||
time: '2024-10-31T14:53:38.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32601
|
||||
- author: Psychpsyo
|
||||
changes:
|
||||
- message: Carp plushies can now be placed in mop buckets, along with other rehydratable
|
||||
things like monkey cubes.
|
||||
type: Add
|
||||
id: 7569
|
||||
time: '2024-10-31T18:46:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33079
|
||||
- author: Bhijn and Myr
|
||||
changes:
|
||||
- message: Tail thumping has been downmixed to mono to fix the sound lacking any
|
||||
sort of positioning. They're now capable of having a presence in the actual
|
||||
soundspace, in turn meaning lizards are no longer occupying your headset at
|
||||
all times of day.
|
||||
type: Fix
|
||||
id: 7570
|
||||
time: '2024-10-31T21:30:58.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33092
|
||||
- author: reesque
|
||||
changes:
|
||||
- message: pie not dropping tin on thrown
|
||||
type: Fix
|
||||
id: 7571
|
||||
time: '2024-11-01T01:43:11.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33013
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: Votekicks can now be initiated during the pregame lobby.
|
||||
type: Fix
|
||||
id: 7572
|
||||
time: '2024-11-01T01:52:55.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32528
|
||||
- author: PopGamer46
|
||||
changes:
|
||||
- message: Fixed bolt lights of recently unpowered bolted doors
|
||||
type: Fix
|
||||
id: 7573
|
||||
time: '2024-11-01T02:04:09.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33063
|
||||
- author: RumiTiger
|
||||
changes:
|
||||
- message: A chocolate and banana muffin has been added to the game. The berry and
|
||||
cherry muffins have also been resprited!
|
||||
type: Add
|
||||
- message: Now you can make a regular, chocolate, banana, and berry muffin!
|
||||
type: Tweak
|
||||
- message: Muffin tins have been added to the game. They are needed for making muffins!
|
||||
type: Add
|
||||
id: 7574
|
||||
time: '2024-11-01T02:06:46.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/29318
|
||||
- author: ScarKy0
|
||||
changes:
|
||||
- message: AI can no longer toggle seeing jobs off.
|
||||
type: Tweak
|
||||
- message: Borgs can no longer see mindshield status.
|
||||
type: Fix
|
||||
id: 7575
|
||||
time: '2024-11-01T02:32:28.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33069
|
||||
- author: Minemoder5000
|
||||
changes:
|
||||
- message: The cargo shuttle's cargo pallets can no longer sell or buy.
|
||||
type: Fix
|
||||
id: 7576
|
||||
time: '2024-11-01T06:22:39.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33022
|
||||
- author: aspiringLich
|
||||
changes:
|
||||
- message: Fixed the logic triggering popups when inserting items into machines.
|
||||
type: Fix
|
||||
id: 7577
|
||||
time: '2024-11-02T01:33:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/28856
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: Pills are explosion resistant.
|
||||
type: Tweak
|
||||
id: 7578
|
||||
time: '2024-11-02T09:51:45.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/32458
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: Handcrafted gauze now takes 3 seconds instead of 10 seconds of crafting
|
||||
@@ -3897,3 +3643,248 @@
|
||||
id: 8047
|
||||
time: '2025-03-10T04:38:33.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35758
|
||||
- author: RedBookcase
|
||||
changes:
|
||||
- message: Added about a dozen new cocktails for the Bartender to mix up, and polished
|
||||
some of the sprites for older drinks.
|
||||
type: Add
|
||||
id: 8048
|
||||
time: '2025-03-10T23:08:24.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33570
|
||||
- author: Velken
|
||||
changes:
|
||||
- message: Performers can now find that their clothing set is more complete!
|
||||
type: Add
|
||||
id: 8049
|
||||
time: '2025-03-10T23:17:24.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35764
|
||||
- author: ScarKy0
|
||||
changes:
|
||||
- message: Mindshield implants can now be removed using an empty implanter.
|
||||
type: Tweak
|
||||
- message: Revolutionaries can now win by converting all command members.
|
||||
type: Add
|
||||
id: 8050
|
||||
time: '2025-03-11T09:41:13.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35769
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: Chameleon PDAs no longer change your name in the station records.
|
||||
type: Fix
|
||||
id: 8051
|
||||
time: '2025-03-11T13:52:38.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35782
|
||||
- author: Reisama
|
||||
changes:
|
||||
- message: Borgs will no longer get laws to prevent the shuttle from being called
|
||||
when Ion Stormed, and may get laws to call the shuttle instead.
|
||||
type: Fix
|
||||
id: 8052
|
||||
time: '2025-03-12T01:21:59.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35751
|
||||
- author: Winkarst-cpu
|
||||
changes:
|
||||
- message: The warden job now rolls before security officer/cadet/detective.
|
||||
type: Tweak
|
||||
id: 8053
|
||||
time: '2025-03-12T22:36:41.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35313
|
||||
- author: Firewars763
|
||||
changes:
|
||||
- message: Added gold and coal rock anomalies and ore crab enemies.
|
||||
type: Add
|
||||
- message: Added yellow and black crystals, crystal shards, and light tubes.
|
||||
type: Add
|
||||
id: 8054
|
||||
time: '2025-03-12T22:45:22.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34809
|
||||
- author: onesch
|
||||
changes:
|
||||
- message: Added handheld sprites to some items
|
||||
type: Add
|
||||
id: 8055
|
||||
time: '2025-03-12T22:46:58.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/33689
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: Nitrous oxide canisters now begin locked like other dangerous gas canisters.
|
||||
type: Fix
|
||||
id: 8056
|
||||
time: '2025-03-13T00:30:17.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35785
|
||||
- author: Plykiya
|
||||
changes:
|
||||
- message: A message now pops up when attempting to fire a gun with no ammo.
|
||||
type: Add
|
||||
id: 8057
|
||||
time: '2025-03-13T07:21:24.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34816
|
||||
- author: Crude Oil
|
||||
changes:
|
||||
- message: You can now pet the mail teleporter
|
||||
type: Add
|
||||
id: 8058
|
||||
time: '2025-03-13T12:49:25.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35819
|
||||
- author: ScarKy0
|
||||
changes:
|
||||
- message: Singularity and Whitehole grenades are now in the Disruption category
|
||||
of uplinks.
|
||||
type: Tweak
|
||||
- message: Singularity and Whitehole grenades have had their prices reduced to 2TC
|
||||
each.
|
||||
type: Tweak
|
||||
- message: Whitehole grenade no longer has extremely overtuned strength.
|
||||
type: Fix
|
||||
id: 8059
|
||||
time: '2025-03-13T13:12:27.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35821
|
||||
- author: slarticodefast, ps3moira
|
||||
changes:
|
||||
- message: Added the paradox clone as a ghostrole antagonist.
|
||||
type: Add
|
||||
id: 8060
|
||||
time: '2025-03-13T17:09:07.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35794
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: All roundstart antags now roll before job selection. You are now able
|
||||
to ready up with antag-immune job preferences (Sec/Command/interns) set to High,
|
||||
and if selected for antag you will get an non-antag-immune job instead.
|
||||
type: Tweak
|
||||
id: 8061
|
||||
time: '2025-03-13T20:21:43.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35823
|
||||
- author: Coolsurf6
|
||||
changes:
|
||||
- message: Added a new and powerful drink called Bacchus' Blessing for all bartenders
|
||||
to struggle with.
|
||||
type: Add
|
||||
id: 8062
|
||||
time: '2025-03-14T06:49:26.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35306
|
||||
- author: Quantum-cross
|
||||
changes:
|
||||
- message: Corrupt borg speech if they are damaged or low power
|
||||
type: Add
|
||||
id: 8063
|
||||
time: '2025-03-14T15:31:10.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35318
|
||||
- author: Centronias
|
||||
changes:
|
||||
- message: Moths can once again eat lizard plushies
|
||||
type: Fix
|
||||
- message: Lizard plushes can once again be inserted as payloads into modular grenades
|
||||
and mines
|
||||
type: Fix
|
||||
id: 8064
|
||||
time: '2025-03-14T20:22:31.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35835
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: Electrocution stun duration decreased overall; LV or hacking from 8 seconds
|
||||
to 5, MV from 10 seconds to 7.5, HV from 12 seconds to 10.
|
||||
type: Tweak
|
||||
id: 8065
|
||||
time: '2025-03-15T15:57:36.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34578
|
||||
- author: sowelipililimute
|
||||
changes:
|
||||
- message: Added NanoTask
|
||||
type: Add
|
||||
id: 8066
|
||||
time: '2025-03-15T16:24:24.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34095
|
||||
- author: K-Dynamic
|
||||
changes:
|
||||
- message: Renamed "shell box (X)" to "ammunition box (.50 X)" to match existing
|
||||
naming scheme, and are thus easier to find in certain vendors and fabricators.
|
||||
type: Tweak
|
||||
id: 8067
|
||||
time: '2025-03-15T16:43:49.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35326
|
||||
- author: VerinSenpai
|
||||
changes:
|
||||
- message: Holopads now collide with walls instead of phasing through them.
|
||||
type: Fix
|
||||
id: 8068
|
||||
time: '2025-03-16T00:41:50.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34300
|
||||
- author: Tayrtahn
|
||||
changes:
|
||||
- message: Cyborgs no longer have their brains removed if the salvage site they
|
||||
are on despawns.
|
||||
type: Fix
|
||||
id: 8069
|
||||
time: '2025-03-16T18:15:39.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35855
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Paradox clones now recieve a copy of (almost) all the inventory items
|
||||
their original has.
|
||||
type: Tweak
|
||||
id: 8070
|
||||
time: '2025-03-16T22:02:19.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35838
|
||||
- author: Errant
|
||||
changes:
|
||||
- message: Some ghostrole antagonists were incorrectly not marked as such in the
|
||||
round end playerlist. Now all antagonists are sorted to the top and have red
|
||||
names.
|
||||
type: Fix
|
||||
id: 8071
|
||||
time: '2025-03-16T22:11:31.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35832
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Paradox clones now receive any objective the original had.
|
||||
type: Add
|
||||
id: 8072
|
||||
time: '2025-03-16T22:24:48.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35829
|
||||
- author: Errant-4
|
||||
changes:
|
||||
- message: The Syndicate Instigator shuttle's crew now has the proper antagonist
|
||||
role type.
|
||||
type: Fix
|
||||
id: 8073
|
||||
time: '2025-03-17T16:27:08.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35892
|
||||
- author: SlamBamActionman
|
||||
changes:
|
||||
- message: The Syndicate Backpack can now fit more items (35 item slots, from 28).
|
||||
type: Tweak
|
||||
id: 8074
|
||||
time: '2025-03-17T17:58:57.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35888
|
||||
- author: beck-thompson
|
||||
changes:
|
||||
- message: Chameleon vests now have the same stats as winter coats (Cold protection,
|
||||
minor heat increase, 5% slash resistance, 10% heat resistance)
|
||||
type: Tweak
|
||||
id: 8075
|
||||
time: '2025-03-17T18:39:04.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/34929
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Paradox clones now receive any implants the original has.
|
||||
type: Add
|
||||
- message: Cloning pods no longer copy items in slime storage.
|
||||
type: Fix
|
||||
id: 8076
|
||||
time: '2025-03-18T05:21:56.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35906
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Tomato Killers now take damage from weedkiller and plant b gone.
|
||||
type: Tweak
|
||||
id: 8077
|
||||
time: '2025-03-18T08:00:44.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35898
|
||||
- author: slarticodefast
|
||||
changes:
|
||||
- message: Paradox Clones now start with their suit sensors turned off.
|
||||
type: Tweak
|
||||
id: 8078
|
||||
time: '2025-03-18T20:07:06.0000000+00:00'
|
||||
url: https://github.com/space-wizards/space-station-14/pull/35909
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
[events]
|
||||
# Annoying
|
||||
enabled = false
|
||||
|
||||
[shuttle]
|
||||
auto_call_time = 0
|
||||
|
||||
@@ -5,6 +5,9 @@ lobbyenabled = false
|
||||
map = "Dev"
|
||||
role_timers = false
|
||||
|
||||
[events]
|
||||
enabled = false
|
||||
|
||||
[ghost]
|
||||
role_time = 0.5
|
||||
quick_lottery = true
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ device-pda-slot-component-slot-name-cartridge = Cartridge
|
||||
|
||||
default-program-name = Program
|
||||
notekeeper-program-name = Notekeeper
|
||||
nano-task-program-name = NanoTask
|
||||
news-read-program-name = Station news
|
||||
|
||||
crew-manifest-program-name = Crew manifest
|
||||
@@ -28,6 +29,47 @@ astro-nav-program-name = AstroNav
|
||||
|
||||
med-tek-program-name = MedTek
|
||||
|
||||
# NanoTask cartridge
|
||||
|
||||
nano-task-ui-heading-high-priority-tasks =
|
||||
{ $amount ->
|
||||
[zero] No High Priority Tasks
|
||||
[one] 1 High Priority Task
|
||||
*[other] {$amount} High Priority Tasks
|
||||
}
|
||||
nano-task-ui-heading-medium-priority-tasks =
|
||||
{ $amount ->
|
||||
[zero] No Medium Priority Tasks
|
||||
[one] 1 Medium Priority Task
|
||||
*[other] {$amount} Medium Priority Tasks
|
||||
}
|
||||
nano-task-ui-heading-low-priority-tasks =
|
||||
{ $amount ->
|
||||
[zero] No Low Priority Tasks
|
||||
[one] 1 Low Priority Task
|
||||
*[other] {$amount} Low Priority Tasks
|
||||
}
|
||||
nano-task-ui-done = Done
|
||||
nano-task-ui-revert-done = Undo
|
||||
nano-task-ui-priority-low = Low
|
||||
nano-task-ui-priority-medium = Medium
|
||||
nano-task-ui-priority-high = High
|
||||
nano-task-ui-cancel = Cancel
|
||||
nano-task-ui-print = Print
|
||||
nano-task-ui-delete = Delete
|
||||
nano-task-ui-save = Save
|
||||
nano-task-ui-new-task = New Task
|
||||
nano-task-ui-description-label = Description:
|
||||
nano-task-ui-description-placeholder = Get something important
|
||||
nano-task-ui-requester-label = Requester:
|
||||
nano-task-ui-requester-placeholder = John Nanotrasen
|
||||
nano-task-ui-item-title = Edit Task
|
||||
nano-task-printed-description = Description: {$description}
|
||||
nano-task-printed-requester = Requester: {$requester}
|
||||
nano-task-printed-high-priority = Priority: High
|
||||
nano-task-printed-medium-priority = Priority: Medium
|
||||
nano-task-printed-low-priority = Priority: Low
|
||||
|
||||
# Wanted list cartridge
|
||||
wanted-list-program-name = Wanted list
|
||||
wanted-list-label-no-records = It's all right, cowboy
|
||||
|
||||
@@ -4,8 +4,5 @@ cmd-showmarkers-help = Usage: {$command}
|
||||
cmd-showsubfloor-desc = Makes entities below the floor always visible.
|
||||
cmd-showsubfloor-help = Usage: {$command}
|
||||
|
||||
cmd-showsubfloorforever-desc = Makes entities below the floor always visible until the client is restarted.
|
||||
cmd-showsubfloorforever-help = Usage: {$command}
|
||||
|
||||
cmd-notify-desc = Send a notify client side.
|
||||
cmd-notify-help = Usage: {$command} <message>
|
||||
|
||||
20
Resources/Locale/en-US/datasets/criminal_records.ftl
Normal file
20
Resources/Locale/en-US/datasets/criminal_records.ftl
Normal file
@@ -0,0 +1,20 @@
|
||||
placeholders-criminal-records-wanted-reason-1 = Ate a delicious valid salad
|
||||
placeholders-criminal-records-wanted-reason-2 = Ate their own shoes
|
||||
placeholders-criminal-records-wanted-reason-3 = Being a clown
|
||||
placeholders-criminal-records-wanted-reason-4 = Being a mime
|
||||
placeholders-criminal-records-wanted-reason-5 = Breathed the wrong way
|
||||
placeholders-criminal-records-wanted-reason-6 = Broke into evac
|
||||
placeholders-criminal-records-wanted-reason-7 = Did literally nothing
|
||||
placeholders-criminal-records-wanted-reason-8 = Did their job
|
||||
placeholders-criminal-records-wanted-reason-9 = Didn't say hello to me
|
||||
placeholders-criminal-records-wanted-reason-10 = Drank one too many
|
||||
placeholders-criminal-records-wanted-reason-11 = Had two toolboxes, that's too many
|
||||
placeholders-criminal-records-wanted-reason-12 = Lied on common radio
|
||||
placeholders-criminal-records-wanted-reason-13 = Looked at me funny
|
||||
placeholders-criminal-records-wanted-reason-14 = Lubed up the entire way to evac
|
||||
placeholders-criminal-records-wanted-reason-15 = Set AME up on time
|
||||
placeholders-criminal-records-wanted-reason-16 = Slipped the HoS
|
||||
placeholders-criminal-records-wanted-reason-17 = Stole the clown's mask
|
||||
placeholders-criminal-records-wanted-reason-18 = Told an unfunny joke
|
||||
placeholders-criminal-records-wanted-reason-19 = Wore a gasmask
|
||||
placeholders-criminal-records-wanted-reason-20 = Wore boxing gloves
|
||||
84
Resources/Locale/en-US/datasets/news.ftl
Normal file
84
Resources/Locale/en-US/datasets/news.ftl
Normal file
@@ -0,0 +1,84 @@
|
||||
news-dataset-1 = Tree stuck in tajaran; firefighters baffled.
|
||||
news-dataset-2 = Armadillos want aardvarks removed from dictionary claims 'here first'.
|
||||
news-dataset-3 = Angel found dancing on pinhead ordered to stop; cited for public nuisance.
|
||||
news-dataset-4 = Letters claim they are better than number; 'Always have been'.
|
||||
news-dataset-5 = Pens proclaim pencils obsolete, 'lead is dead'.
|
||||
news-dataset-6 = Rock and paper sues scissors for discrimination.
|
||||
news-dataset-7 = Steak tell-all book reveals he never liked sitting by potato.
|
||||
news-dataset-8 = Woodchuck stops counting how many times he’s chucked 'Never again'.
|
||||
news-dataset-9 = 'Here kitty kitty' no longer preferred tajaran retrieval technique.
|
||||
news-dataset-10 = Man travels 7000 light years to retrieve lost hankie, 'It was my favourite'.
|
||||
news-dataset-11 = New bowling lane that shoots mini-meteors at bowlers very popular.
|
||||
news-dataset-12 = Skrell marries computer; wedding attended by 100 modems.
|
||||
news-dataset-13 = Chef reports successfully using harmonica as cheese grater.
|
||||
news-dataset-14 = Nanotrasen invents handkerchief that says 'Bless you' after sneeze.
|
||||
news-dataset-15 = Clone accused of posing for other clones’s school photo.
|
||||
news-dataset-16 = Clone accused of stealing other clones’s employee of the month award.
|
||||
news-dataset-17 = Woman robs station with hair dryer; crewmen love new style.
|
||||
news-dataset-18 = This space for rent.
|
||||
news-dataset-19 = Skrell Scientist Discovers Abacus Can Be Used To Dry Towels
|
||||
news-dataset-20 = Survey: 'Cheese Louise' Voted Best Pizza Restaurant In Tau Ceti
|
||||
news-dataset-21 = Swamp Gas Verified To Be Exhalations Of Stars--Movie Stars--Long Passed
|
||||
news-dataset-22 = Tainted Broccoli Weapon Of Choice For Syndicate Assassins
|
||||
news-dataset-23 = Chefs Find Broccoli Effective Tool For Cutting Cheese
|
||||
news-dataset-24 = Broccoli Found To Cause Grumpiness In Monkeys
|
||||
news-dataset-25 = Giant Hairball Has Perfect Grammar But Rolls rr's Too Much, Linguists Say
|
||||
news-dataset-26 = Gibson Gazette Updates Frequently Absurd, Poll Indicates
|
||||
news-dataset-27 = Taj Demand Longer Breaks, Cleaner Litter, Slower Mice
|
||||
news-dataset-28 = Survey: 3 Out Of 5 Skrell Loathe Modern Art
|
||||
news-dataset-29 = Skrell Scientist Discovers Gravity While Falling Down Stairs
|
||||
news-dataset-30 = Humans Everywhere Agree: Purring Tajarans Are Happy Tajarans
|
||||
news-dataset-31 = From The Desk Of Wise Guy Sammy: One Word In This Gazette Is Sdrawkcab
|
||||
news-dataset-32 = From The Desk Of Wise Guy Sammy: It's Hard To Have Too Much Shelf Space
|
||||
news-dataset-33 = From The Desk Of Wise Guy Sammy: Wine And Friendships Get Better With Age
|
||||
news-dataset-34 = From The Desk Of Wise Guy Sammy: The Insides Of Golf Balls Are Mostly Rubber Bands
|
||||
news-dataset-35 = From The Desk Of Wise Guy Sammy: You Don't Have To Fool All The People, Just The Right Ones
|
||||
news-dataset-36 = From The Desk Of Wise Guy Sammy: If You Made The Mess, You Clean It Up
|
||||
news-dataset-37 = From The Desk Of Wise Guy Sammy: It Is Easier To Get Forgiveness Than Permission
|
||||
news-dataset-38 = From The Desk Of Wise Guy Sammy: Check Your Facts Before Making A Fool Of Yourself
|
||||
news-dataset-39 = From The Desk Of Wise Guy Sammy: You Can't Outwait A Bureaucracy
|
||||
news-dataset-40 = From The Desk Of Wise Guy Sammy: It's Better To Yield Right Of Way Than To Demand It
|
||||
news-dataset-41 = From The Desk Of Wise Guy Sammy: A Person Who Likes Cats Can't Be All Bad
|
||||
news-dataset-42 = From The Desk Of Wise Guy Sammy: Help Is The Sunny Side Of Control
|
||||
news-dataset-43 = From The Desk Of Wise Guy Sammy: Two Points Determine A Straight Line
|
||||
news-dataset-44 = From The Desk Of Wise Guy Sammy: Reading Improves The Mind And Lifts The Spirit
|
||||
news-dataset-45 = From The Desk Of Wise Guy Sammy: Better To Aim High And Miss Then To Aim Low And Hit
|
||||
news-dataset-46 = From The Desk Of Wise Guy Sammy: Meteors Often Strike The Same Place More Than Once
|
||||
news-dataset-47 = Tommy B. Saif Sez: Look Both Ways Before Boarding The Shuttle
|
||||
news-dataset-48 = Tommy B. Saif Sez: Hold On; Sudden Stops Sometimes Necessary
|
||||
news-dataset-49 = Tommy B. Saif Sez: Keep Fingers Away From Moving Panels
|
||||
news-dataset-50 = Tommy B. Saif Sez: No Left Turn, Except Shuttles
|
||||
news-dataset-51 = Tommy B. Saif Sez: Return Seats And Trays To Their Proper Upright Position
|
||||
news-dataset-52 = Tommy B. Saif Sez: Eating And Drinking In Docking Bays Is Prohibited
|
||||
news-dataset-53 = Tommy B. Saif Sez: Accept No Substitutes, And Don't Be Fooled By Imitations
|
||||
news-dataset-54 = Tommy B. Saif Sez: Do Not Remove This Tag Under Penalty Of Law
|
||||
news-dataset-55 = Tommy B. Saif Sez: Always Mix Thoroughly When So Instructed
|
||||
news-dataset-56 = Tommy B. Saif Sez: Try To Keep Six Month's Expenses In Reserve
|
||||
news-dataset-57 = Tommy B. Saif Sez: Change Not Given Without Purchase
|
||||
news-dataset-58 = Tommy B. Saif Sez: If You Break It, You Buy It
|
||||
news-dataset-59 = Tommy B. Saif Sez: Reservations Must Be Cancelled 48 Hours Prior To Event To Obtain Refund
|
||||
news-dataset-60 = Doughnuts: Is There Anything They Can't Do
|
||||
news-dataset-61 = If Tin Whistles Are Made Of Tin, What Do They Make Foghorns Out Of?
|
||||
news-dataset-62 = Broccoli discovered to be colonies of tiny aliens with murder on their minds
|
||||
|
||||
## Commented
|
||||
# {{AFFECTED}} clerk first person able to pronounce '@*$%!'.
|
||||
# {{AFFECTED}} delis serving boiled paperback dictionaries, 'Adjectives chewy' customers declare.
|
||||
# {{AFFECTED}} weather deemed 'boring'; meteors and rad storms to be imported.
|
||||
# Most {{AFFECTED}} security officers prefer cream over sugar.
|
||||
# Palindrome speakers conference in {{AFFECTED}}; 'Wow!' says Otto.
|
||||
# Question mark worshipped as deity by ancient {{AFFECTED}} dwellers.
|
||||
# Spilled milk causes whole {{AFFECTED}} populace to cry.
|
||||
# World largest carp patty at display on {{AFFECTED}}.
|
||||
# Guy gets tattoo of Tau Ceti on chest '[pick(CentCom,star,starship,asteroid)] tickles most'.
|
||||
# {{AFFECTED}} Baker Wins Pickled Crumpet Toss Three Years Running
|
||||
# I Was Framed, jokes {{AFFECTED}} artist
|
||||
# Mysterious Loud Rumbling Noises In {{AFFECTED}} Found To Be Mysterious Loud Rumblings
|
||||
# Alien ambassador becomes lost on {{AFFECTED}}, refuses to ask for directions
|
||||
# Survey: 80% Of People on {{AFFECTED}} Love Clog-Dancing
|
||||
# {{AFFECTED}} Phonebooks Print All Wrong Numbers; Results In 15 New Marriages
|
||||
# Tajaran Burglar Spotted on {{AFFECTED}}, Mistaken For Dalmatian
|
||||
# Esoteric Verbosity Culminates In Communicative Ennui, {{AFFECTED}} Academics Note
|
||||
# Boy Saves Tajaran From Tree on {{AFFECTED}}, Thousands Cheer
|
||||
# Shipment Of Apples Overturns, {{AFFECTED}} Diner Offers Applesauce Special
|
||||
# Spotted Owl Spotted on {{AFFECTED}}
|
||||
14
Resources/Locale/en-US/datasets/rat_king_commands.ftl
Normal file
14
Resources/Locale/en-US/datasets/rat_king_commands.ftl
Normal file
@@ -0,0 +1,14 @@
|
||||
rat-king-command-stay-1 = Sit!
|
||||
rat-king-command-stay-2 = Stay!
|
||||
rat-king-command-stay-3 = Stop!
|
||||
|
||||
rat-king-command-follow-1 = Heel!
|
||||
rat-king-command-follow-2 = Follow!
|
||||
|
||||
rat-king-command-cheese-1 = Attack!
|
||||
rat-king-command-cheese-2 = Sic!
|
||||
rat-king-command-cheese-3 = Kill!
|
||||
rat-king-command-cheese-4 = Cheese 'Em!
|
||||
|
||||
rat-king-command-loose-1 = Free!
|
||||
rat-king-command-loose-2 = Loose!
|
||||
@@ -264,6 +264,17 @@ flavor-complex-themartinez = like violets and lemon vodka
|
||||
flavor-complex-cogchamp = like brass
|
||||
flavor-complex-white-gilgamesh = like lightly carbonated cream
|
||||
flavor-complex-antifreeze = warm
|
||||
flavor-complex-caipirinha = like Brazil
|
||||
flavor-complex-daiquiri = like rum, lime and sugar
|
||||
flavor-complex-deathintheafternoon = like anise and champagne
|
||||
flavor-complex-empress75 = like tyrian purple
|
||||
flavor-complex-espressomartini = like vodka and coffee
|
||||
flavor-complex-mayojito = like stomach turmoil
|
||||
flavor-complex-mimeosa = like silence and oranges
|
||||
flavor-complex-mimosa = like an early brunch
|
||||
flavor-complex-moscowmule = like vodka and ginger ale
|
||||
flavor-complex-thesunalsorises = like an absinthe daiquiri
|
||||
flavor-complex-whiskeysour = like whiskey and egg
|
||||
flavor-complex-zombiecocktail = like eating brains
|
||||
flavor-complex-absinthe = like anise
|
||||
flavor-complex-blue-curacao = like orange flowers
|
||||
@@ -301,6 +312,7 @@ flavor-complex-toxins-special = like space exploration
|
||||
flavor-complex-vodka-martini = like a spy movie from Russia
|
||||
flavor-complex-vodka-tonic = refreshingly bitter
|
||||
flavor-complex-coconut-rum = like nutty fermented sugar
|
||||
flavor-complex-bacchus-blessing = like a wall of bricks
|
||||
|
||||
### This is exactly what pilk tastes like. I'm not even joking. I might've been a little drunk though
|
||||
flavor-complex-pilk = like sweet milk
|
||||
|
||||
@@ -5,28 +5,28 @@ roles-antag-rev-head-objective = Your objective is to take over the station by c
|
||||
|
||||
head-rev-role-greeting =
|
||||
You are a Head Revolutionary.
|
||||
You are tasked with removing all of Command from station via death, exilement or imprisonment.
|
||||
You are tasked with removing all of Command from station via conversion, death or imprisonment.
|
||||
The Syndicate has sponsored you with a flash that converts the crew to your side.
|
||||
Beware, this won't work on Security, Command, or those wearing sunglasses.
|
||||
Beware, this won't work on those with a mindshield or wearing eye protection.
|
||||
Viva la revolución!
|
||||
|
||||
head-rev-briefing =
|
||||
Use flashes to convert people to your cause.
|
||||
Get rid of all heads to take over the station.
|
||||
Get rid of or convert all heads to take over the station.
|
||||
|
||||
head-rev-break-mindshield = The Mindshield was destroyed!
|
||||
|
||||
## Rev
|
||||
|
||||
roles-antag-rev-name = Revolutionary
|
||||
roles-antag-rev-objective = Your objective is to ensure the safety and follow the orders of the Head Revolutionaries as well as getting rid of all Command staff on station.
|
||||
roles-antag-rev-objective = Your objective is to ensure the safety and follow the orders of the Head Revolutionaries as well as getting rid or converting of all Command staff on station.
|
||||
|
||||
rev-break-control = {$name} has remembered their true allegiance!
|
||||
|
||||
rev-role-greeting =
|
||||
You are a Revolutionary.
|
||||
You are tasked with taking over the station and protecting the Head Revolutionaries.
|
||||
Get rid of all of the Command staff.
|
||||
Get rid of all of or convert the Command staff.
|
||||
Viva la revolución!
|
||||
|
||||
rev-briefing = Help your head revolutionaries get rid of every head to take over the station.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user