mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-17 22:31:00 +03:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # Content.Shared/Interaction/SharedInteractionSystem.cs
This commit is contained in:
commit
5b9ec5ce3e
469 changed files with 7363 additions and 3397 deletions
1
.github/workflows/conflict-labeler.yml
vendored
1
.github/workflows/conflict-labeler.yml
vendored
|
|
@ -9,6 +9,7 @@ on:
|
|||
jobs:
|
||||
Label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.draft == false
|
||||
steps:
|
||||
- name: Check for Merge Conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@513a24fc7dca40990863be2935e059e650728400
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<ui:VoteCallMenuButton />
|
||||
<Button Name="OptionsButton" Text="{Loc 'ui-escape-options'}" />
|
||||
<Button Name="RulesButton" Text="{Loc 'ui-escape-rules'}" />
|
||||
<Button Name="WikiButton" Text="{Loc 'ui-escape-wiki'}" />
|
||||
<Button Name="DisconnectButton" Text="{Loc 'ui-escape-disconnect'}" />
|
||||
<Button Name="QuitButton" Text="{Loc 'ui-escape-quit'}" />
|
||||
</BoxContainer>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using Robust.Client.Console;
|
|||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
using Content.Client.Links;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.EscapeMenu.UI
|
||||
|
|
@ -28,6 +30,7 @@ namespace Content.Client.EscapeMenu.UI
|
|||
QuitButton.OnPressed += OnQuitButtonClicked;
|
||||
RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
|
||||
DisconnectButton.OnPressed += OnDisconnectButtonClicked;
|
||||
WikiButton.OnPressed += OnWikiButtonClicked;
|
||||
}
|
||||
|
||||
private void OnQuitButtonClicked(BaseButton.ButtonEventArgs args)
|
||||
|
|
@ -47,6 +50,12 @@ namespace Content.Client.EscapeMenu.UI
|
|||
_optionsMenu.OpenCentered();
|
||||
}
|
||||
|
||||
private void OnWikiButtonClicked(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
var uriOpener = IoCManager.Resolve<IUriOpener>();
|
||||
uriOpener.OpenUri(UILinks.Wiki);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,38 @@
|
|||
using System;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client.Ghost.Roles.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GhostRoleRulesWindow : DefaultWindow
|
||||
{
|
||||
private float _timer = 5.0f;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = IoCManager.Resolve<IConfigurationManager>();
|
||||
private float _timer;
|
||||
|
||||
public GhostRoleRulesWindow(string rules, Action<BaseButton.ButtonEventArgs> requestAction)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
TopBanner.SetMessage(FormattedMessage.FromMarkupPermissive(rules + "\n" + Loc.GetString("ghost-roles-window-rules-footer")));
|
||||
var ghostRoleTime = _cfg.GetCVar(CCVars.GhostRoleTime);
|
||||
_timer = ghostRoleTime;
|
||||
|
||||
if (ghostRoleTime > 0f)
|
||||
{
|
||||
RequestButton.Text = Loc.GetString("ghost-roles-window-request-role-button-timer", ("time", $"{_timer:0.0}"));
|
||||
TopBanner.SetMessage(FormattedMessage.FromMarkupPermissive(rules + "\n" + Loc.GetString("ghost-roles-window-rules-footer", ("time", ghostRoleTime))));
|
||||
RequestButton.Disabled = true;
|
||||
}
|
||||
|
||||
RequestButton.OnPressed += requestAction;
|
||||
}
|
||||
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
|
@ -28,10 +40,12 @@ namespace Content.Client.Ghost.Roles.UI
|
|||
if (_timer > 0.0)
|
||||
{
|
||||
_timer -= args.DeltaSeconds;
|
||||
RequestButton.Text = Loc.GetString("ghost-roles-window-request-role-button-timer", ("time", $"{_timer:0.0}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestButton.Disabled = false;
|
||||
RequestButton.Text = Loc.GetString("ghost-roles-window-request-role-button");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Shared.MobState;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.MobState
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class DamageStateVisualizer : AppearanceVisualizer, ISerializationHooks
|
||||
{
|
||||
private DamageState _data = DamageState.Alive;
|
||||
private Dictionary<DamageState, string> _stateMap = new();
|
||||
private int? _originalDrawDepth;
|
||||
|
||||
[DataField("normal")]
|
||||
private string? _normal;
|
||||
|
||||
[DataField("crit")]
|
||||
private string? _crit;
|
||||
|
||||
[DataField("dead")]
|
||||
private string? _dead;
|
||||
|
||||
/// <summary>
|
||||
/// Should noRot be turned off when crit / dead.
|
||||
/// </summary>
|
||||
[DataField("rotate")]
|
||||
private bool _rotate;
|
||||
|
||||
void ISerializationHooks.BeforeSerialization()
|
||||
{
|
||||
_stateMap.TryGetValue(DamageState.Alive, out _normal);
|
||||
_stateMap.TryGetValue(DamageState.Critical, out _crit);
|
||||
_stateMap.TryGetValue(DamageState.Dead, out _dead);
|
||||
}
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
if (_normal != null)
|
||||
{
|
||||
_stateMap.Add(DamageState.Alive, _normal);
|
||||
}
|
||||
|
||||
if (_crit != null)
|
||||
{
|
||||
_stateMap.Add(DamageState.Critical, _crit);
|
||||
}
|
||||
|
||||
if (_dead != null)
|
||||
{
|
||||
_stateMap.Add(DamageState.Dead, _dead);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
|
||||
if (!component.TryGetData(DamageStateVisuals.State, out DamageState data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_data == data)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_data = data;
|
||||
|
||||
if (_rotate)
|
||||
{
|
||||
sprite.NoRotation = data switch
|
||||
{
|
||||
DamageState.Critical => false,
|
||||
DamageState.Dead => false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
if (_stateMap.TryGetValue(_data, out var state))
|
||||
{
|
||||
sprite.LayerSetState(DamageStateVisualLayers.Base, state);
|
||||
}
|
||||
|
||||
// So they don't draw over mobs anymore
|
||||
if (_data == DamageState.Dead && sprite.DrawDepth > (int) DrawDepth.Items)
|
||||
{
|
||||
_originalDrawDepth = sprite.DrawDepth;
|
||||
sprite.DrawDepth = (int) DrawDepth.Items;
|
||||
}
|
||||
else if (_originalDrawDepth != null)
|
||||
{
|
||||
sprite.DrawDepth = _originalDrawDepth.Value;
|
||||
_originalDrawDepth = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum DamageStateVisualLayers : byte
|
||||
{
|
||||
Base
|
||||
}
|
||||
}
|
||||
62
Content.Client/MobState/DamageStateVisualizerSystem.cs
Normal file
62
Content.Client/MobState/DamageStateVisualizerSystem.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Content.Shared.MobState;
|
||||
using Robust.Client.GameObjects;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.MobState;
|
||||
|
||||
public sealed class DamageStateVisualizerSystem : VisualizerSystem<DamageStateVisualsComponent>
|
||||
{
|
||||
protected override void OnAppearanceChange(EntityUid uid, DamageStateVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
var sprite = args.Sprite;
|
||||
|
||||
if (sprite == null || !args.Component.TryGetData(DamageStateVisuals.State, out DamageState data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!component.States.TryGetValue(data, out var layers))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.Rotate)
|
||||
{
|
||||
sprite.NoRotation = data switch
|
||||
{
|
||||
DamageState.Critical => false,
|
||||
DamageState.Dead => false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
// Brain no worky rn so this was just easier.
|
||||
foreach (var key in new []{ DamageStateVisualLayers.Base, DamageStateVisualLayers.BaseUnshaded })
|
||||
{
|
||||
if (!sprite.LayerMapTryGet(key, out _)) continue;
|
||||
|
||||
sprite.LayerSetVisible(key, false);
|
||||
}
|
||||
|
||||
foreach (var (key, state) in layers)
|
||||
{
|
||||
// Inheritance moment.
|
||||
if (!sprite.LayerMapTryGet(key, out _)) continue;
|
||||
|
||||
sprite.LayerSetVisible(key, true);
|
||||
sprite.LayerSetState(key, state);
|
||||
}
|
||||
|
||||
// So they don't draw over mobs anymore
|
||||
if (data == DamageState.Dead && sprite.DrawDepth > (int) DrawDepth.Items)
|
||||
{
|
||||
component.OriginalDrawDepth = sprite.DrawDepth;
|
||||
sprite.DrawDepth = (int) DrawDepth.Items;
|
||||
}
|
||||
else if (component.OriginalDrawDepth != null)
|
||||
{
|
||||
sprite.DrawDepth = component.OriginalDrawDepth.Value;
|
||||
component. OriginalDrawDepth = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Content.Client/MobState/DamageStateVisualsComponent.cs
Normal file
22
Content.Client/MobState/DamageStateVisualsComponent.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Content.Shared.MobState;
|
||||
|
||||
namespace Content.Client.MobState;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class DamageStateVisualsComponent : Component
|
||||
{
|
||||
public int? OriginalDrawDepth;
|
||||
|
||||
[DataField("states")] public Dictionary<DamageState, Dictionary<DamageStateVisualLayers, string>> States = new();
|
||||
|
||||
/// <summary>
|
||||
/// Should noRot be turned off when crit / dead.
|
||||
/// </summary>
|
||||
[DataField("rotate")] public bool Rotate;
|
||||
}
|
||||
|
||||
public enum DamageStateVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
BaseUnshaded,
|
||||
}
|
||||
|
|
@ -1,7 +1,38 @@
|
|||
using Content.Shared.PowerCell;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class PowerCellSystem : SharedPowerCellSystem { }
|
||||
public sealed class PowerCellSystem : SharedPowerCellSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
|
||||
}
|
||||
|
||||
private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null) return;
|
||||
|
||||
if (args.Component.TryGetData(PowerCellVisuals.ChargeLevel, out byte level))
|
||||
{
|
||||
if (level == 0)
|
||||
{
|
||||
args.Sprite.LayerSetVisible(PowerCellVisualLayers.Unshaded, false);
|
||||
return;
|
||||
}
|
||||
|
||||
args.Sprite.LayerSetVisible(PowerCellVisualLayers.Unshaded, true);
|
||||
args.Sprite.LayerSetState(PowerCellVisualLayers.Unshaded, $"o{level}");
|
||||
}
|
||||
}
|
||||
|
||||
private enum PowerCellVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Unshaded,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
using Content.Shared.PowerCell;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Client.PowerCell
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class PowerCellVisualizer : AppearanceVisualizer
|
||||
{
|
||||
[DataField("prefix")]
|
||||
private string? _prefix;
|
||||
|
||||
public override void InitializeEntity(EntityUid entity)
|
||||
{
|
||||
base.InitializeEntity(entity);
|
||||
|
||||
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(entity);
|
||||
|
||||
if (_prefix != null)
|
||||
{
|
||||
sprite.LayerMapSet(Layers.Charge, sprite.AddLayerState($"{_prefix}_100"));
|
||||
sprite.LayerSetShader(Layers.Charge, "unshaded");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
|
||||
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
|
||||
if (component.TryGetData(PowerCellVisuals.ChargeLevel, out byte level))
|
||||
{
|
||||
var adjustedLevel = level * 25;
|
||||
sprite.LayerSetState(Layers.Charge, $"{_prefix}_{adjustedLevel}");
|
||||
}
|
||||
}
|
||||
|
||||
private enum Layers : byte
|
||||
{
|
||||
Charge
|
||||
}
|
||||
}
|
||||
}
|
||||
4
Content.Client/PowerCell/PowerCellVisualsComponent.cs
Normal file
4
Content.Client/PowerCell/PowerCellVisualsComponent.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace Content.Client.PowerCell;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class PowerCellVisualsComponent : Component {}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:radar="clr-namespace:Content.Client.Radar"
|
||||
Title="{Loc 'radar-window-title'}"
|
||||
Resizable="False">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<radar:RadarControl Name="Radar"/>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
using System;
|
||||
using Content.Client.Computer;
|
||||
using Content.Shared.Radar;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.Radar;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RadarConsoleWindow : DefaultWindow, IComputerWindow<RadarConsoleBoundInterfaceState>
|
||||
{
|
||||
public RadarConsoleWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetupComputerWindow(ComputerBoundUserInterfaceBase cb)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void UpdateState(RadarConsoleBoundInterfaceState scc)
|
||||
{
|
||||
Radar.UpdateState(scc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed class RadarControl : Control
|
||||
{
|
||||
private const int MinimapRadius = 256;
|
||||
private const int MinimapMargin = 4;
|
||||
private const float GridLinesDistance = 32f;
|
||||
|
||||
private float _radarRange = 256f;
|
||||
private RadarConsoleBoundInterfaceState _lastState = new(256f, Array.Empty<RadarObjectData>());
|
||||
|
||||
private int SizeFull => (int) ((MinimapRadius + MinimapMargin) * 2 * UIScale);
|
||||
private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
|
||||
private float MinimapScale => _radarRange != 0 ? ScaledMinimapRadius / _radarRange : 0f;
|
||||
|
||||
public RadarControl()
|
||||
{
|
||||
MinSize = (SizeFull, SizeFull);
|
||||
}
|
||||
|
||||
public void UpdateState(RadarConsoleBoundInterfaceState ls)
|
||||
{
|
||||
if (!_radarRange.Equals(ls.Range))
|
||||
{
|
||||
_radarRange = ls.Range;
|
||||
}
|
||||
|
||||
_lastState = ls;
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
var point = SizeFull / 2;
|
||||
var fakeAA = new Color(0.08f, 0.08f, 0.08f);
|
||||
var gridLines = new Color(0.08f, 0.08f, 0.08f);
|
||||
var gridLinesRadial = 8;
|
||||
var gridLinesEquatorial = (int) Math.Floor(_radarRange / GridLinesDistance);
|
||||
|
||||
handle.DrawCircle((point, point), ScaledMinimapRadius + 1, fakeAA);
|
||||
handle.DrawCircle((point, point), ScaledMinimapRadius, Color.Black);
|
||||
|
||||
for (var i = 1; i < gridLinesEquatorial + 1; i++)
|
||||
{
|
||||
handle.DrawCircle((point, point), GridLinesDistance * MinimapScale * i, gridLines, false);
|
||||
}
|
||||
|
||||
for (var i = 0; i < gridLinesRadial; i++)
|
||||
{
|
||||
Angle angle = (Math.PI / gridLinesRadial) * i;
|
||||
var aExtent = angle.ToVec() * ScaledMinimapRadius;
|
||||
handle.DrawLine((point, point) - aExtent, (point, point) + aExtent, gridLines);
|
||||
}
|
||||
|
||||
handle.DrawLine((point, point) + new Vector2(8, 8), (point, point) - new Vector2(0, 8), Color.Yellow);
|
||||
handle.DrawLine((point, point) + new Vector2(-8, 8), (point, point) - new Vector2(0, 8), Color.Yellow);
|
||||
|
||||
foreach (var obj in _lastState.Objects)
|
||||
{
|
||||
var minimapPos = obj.Position * MinimapScale;
|
||||
var radius = obj.Radius * MinimapScale;
|
||||
|
||||
if (minimapPos.Length + radius > ScaledMinimapRadius)
|
||||
continue;
|
||||
|
||||
switch (obj.Shape)
|
||||
{
|
||||
case RadarObjectShape.CircleFilled:
|
||||
case RadarObjectShape.Circle:
|
||||
{
|
||||
handle.DrawCircle(minimapPos + point, radius, obj.Color, obj.Shape == RadarObjectShape.CircleFilled);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class RadarConsoleBoundUserInterface : ComputerBoundUserInterface<RadarConsoleWindow, RadarConsoleBoundInterfaceState>
|
||||
{
|
||||
public RadarConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Content.Client.Shuttles.UI;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Shuttles.BUI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class RadarConsoleBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private RadarConsoleWindow? _window;
|
||||
|
||||
public RadarConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = new RadarConsoleWindow();
|
||||
_window?.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (state is not RadarConsoleBoundInterfaceState cState) return;
|
||||
|
||||
_window?.UpdateState(cState);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
using Content.Client.Shuttles.UI;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Shuttles.BUI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ShuttleConsoleBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private ShuttleConsoleWindow? _window;
|
||||
|
||||
public ShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_window = new ShuttleConsoleWindow();
|
||||
_window.ShuttleModePressed += OnShuttleModePressed;
|
||||
_window.UndockPressed += OnUndockPressed;
|
||||
_window.StartAutodockPressed += OnAutodockPressed;
|
||||
_window.StopAutodockPressed += OnStopAutodockPressed;
|
||||
_window.OpenCentered();
|
||||
_window.OnClose += OnClose;
|
||||
}
|
||||
|
||||
private void OnClose()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_window?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStopAutodockPressed(EntityUid obj)
|
||||
{
|
||||
SendMessage(new StopAutodockRequestMessage() {Entity = obj});
|
||||
}
|
||||
|
||||
private void OnAutodockPressed(EntityUid obj)
|
||||
{
|
||||
SendMessage(new AutodockRequestMessage() {Entity = obj});
|
||||
}
|
||||
|
||||
private void OnUndockPressed(EntityUid obj)
|
||||
{
|
||||
SendMessage(new UndockRequestMessage() {Entity = obj});
|
||||
}
|
||||
|
||||
private void OnShuttleModePressed(ShuttleMode obj)
|
||||
{
|
||||
SendMessage(new ShuttleModeRequestMessage() {Mode = obj});
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
if (state is not ShuttleConsoleBoundInterfaceState cState) return;
|
||||
_window?.UpdateState(cState);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
using Content.Shared.Shuttles;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.Shuttles
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedShuttleConsoleComponent))]
|
||||
internal sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent
|
||||
{
|
||||
|
||||
}
|
||||
public sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
using Content.Shared.Shuttles;
|
||||
|
||||
namespace Content.Client.Shuttles
|
||||
{
|
||||
internal sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
21
Content.Client/Shuttles/Systems/DockingSystem.cs
Normal file
21
Content.Client/Shuttles/Systems/DockingSystem.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Content.Shared.Shuttles.Events;
|
||||
|
||||
namespace Content.Client.Shuttles.Systems;
|
||||
|
||||
public sealed class DockingSystem : EntitySystem
|
||||
{
|
||||
public void StartAutodock(EntityUid uid)
|
||||
{
|
||||
RaiseNetworkEvent(new AutodockRequestMessage {Entity = uid});
|
||||
}
|
||||
|
||||
public void StopAutodock(EntityUid uid)
|
||||
{
|
||||
RaiseNetworkEvent(new StopAutodockRequestMessage() {Entity = uid});
|
||||
}
|
||||
|
||||
public void Undock(EntityUid uid)
|
||||
{
|
||||
RaiseNetworkEvent(new UndockRequestMessage() {Entity = uid});
|
||||
}
|
||||
}
|
||||
8
Content.Client/Shuttles/Systems/RadarConsoleSystem.cs
Normal file
8
Content.Client/Shuttles/Systems/RadarConsoleSystem.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
using Content.Shared.Shuttles.Systems;
|
||||
|
||||
namespace Content.Client.Shuttles.Systems;
|
||||
|
||||
public sealed class RadarConsoleSystem : SharedRadarConsoleSystem
|
||||
{
|
||||
|
||||
}
|
||||
37
Content.Client/Shuttles/Systems/ShuttleConsoleSystem.cs
Normal file
37
Content.Client/Shuttles/Systems/ShuttleConsoleSystem.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Shuttles.Systems
|
||||
{
|
||||
public sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PilotComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, PilotComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not PilotComponentState state) return;
|
||||
|
||||
var console = state.Console.GetValueOrDefault();
|
||||
if (!console.IsValid())
|
||||
{
|
||||
component.Console = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<ShuttleConsoleComponent>(console, out var shuttleConsoleComponent))
|
||||
{
|
||||
Logger.Warning($"Unable to set Helmsman console to {console}");
|
||||
return;
|
||||
}
|
||||
|
||||
component.Console = shuttleConsoleComponent;
|
||||
ActionBlockerSystem.UpdateCanMove(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
251
Content.Client/Shuttles/UI/DockingControl.cs
Normal file
251
Content.Client/Shuttles/UI/DockingControl.cs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Displays the docking view from a specific docking port
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class DockingControl : Control
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IMapManager _mapManager;
|
||||
|
||||
private const int MinimapRadius = 384;
|
||||
private const int MinimapMargin = 4;
|
||||
|
||||
private float _range = 8f;
|
||||
private float _rangeSquared = 0f;
|
||||
private const float GridLinesDistance = 32f;
|
||||
|
||||
private int MidPoint => SizeFull / 2;
|
||||
private int SizeFull => (int) ((MinimapRadius + MinimapMargin) * 2 * UIScale);
|
||||
private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
|
||||
private float MinimapScale => _range != 0 ? ScaledMinimapRadius / _range : 0f;
|
||||
|
||||
public EntityUid? ViewedDock;
|
||||
public EntityUid? GridEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Stored by GridID then by docks
|
||||
/// </summary>
|
||||
public Dictionary<EntityUid, List<DockingInterfaceState>> Docks = new();
|
||||
|
||||
public DockingControl()
|
||||
{
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
_rangeSquared = _range * _range;
|
||||
MinSize = (SizeFull, SizeFull);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
var fakeAA = new Color(0.08f, 0.08f, 0.08f);
|
||||
|
||||
handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
|
||||
handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
|
||||
|
||||
var gridLines = new Color(0.08f, 0.08f, 0.08f);
|
||||
var gridLinesRadial = 8;
|
||||
var gridLinesEquatorial = (int) Math.Floor(_range / GridLinesDistance);
|
||||
|
||||
for (var i = 1; i < gridLinesEquatorial + 1; i++)
|
||||
{
|
||||
handle.DrawCircle((MidPoint, MidPoint), GridLinesDistance * MinimapScale * i, gridLines, false);
|
||||
}
|
||||
|
||||
for (var i = 0; i < gridLinesRadial; i++)
|
||||
{
|
||||
Angle angle = (Math.PI / gridLinesRadial) * i;
|
||||
var aExtent = angle.ToVec() * ScaledMinimapRadius;
|
||||
handle.DrawLine((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
|
||||
}
|
||||
|
||||
if (!_entManager.TryGetComponent<TransformComponent>(ViewedDock, out var xform) ||
|
||||
!_entManager.TryGetComponent<TransformComponent>(GridEntity, out var gridXform)) return;
|
||||
|
||||
var rotation = Matrix3.CreateRotation(xform.LocalRotation);
|
||||
var matrix = Matrix3.CreateTranslation(-xform.LocalPosition);
|
||||
|
||||
// Draw the fixtures around the dock before drawing it
|
||||
if (_entManager.TryGetComponent<FixturesComponent>(GridEntity, out var fixtures))
|
||||
{
|
||||
foreach (var (_, fixture) in fixtures.Fixtures)
|
||||
{
|
||||
var poly = (PolygonShape) fixture.Shape;
|
||||
|
||||
for (var i = 0; i < poly.VertexCount; i++)
|
||||
{
|
||||
var start = matrix.Transform(poly.Vertices[i]);
|
||||
var end = matrix.Transform(poly.Vertices[(i + 1) % poly.VertexCount]);
|
||||
|
||||
var startOut = start.LengthSquared > _rangeSquared;
|
||||
var endOut = end.LengthSquared > _rangeSquared;
|
||||
|
||||
// We need to draw to the radar border so we'll cap the range,
|
||||
// but if none of the verts are in range then just leave it.
|
||||
if (startOut && endOut)
|
||||
continue;
|
||||
|
||||
start.Y = -start.Y;
|
||||
end.Y = -end.Y;
|
||||
|
||||
// If start is outside we draw capped from end to start
|
||||
if (startOut)
|
||||
{
|
||||
// It's called Jobseeker now.
|
||||
if (!MathHelper.TryGetIntersecting(start, end, _range, out var newStart)) continue;
|
||||
start = newStart.Value;
|
||||
}
|
||||
// otherwise vice versa
|
||||
else if (endOut)
|
||||
{
|
||||
if (!MathHelper.TryGetIntersecting(end, start, _range, out var newEnd)) continue;
|
||||
end = newEnd.Value;
|
||||
}
|
||||
|
||||
handle.DrawLine(ScalePosition(start), ScalePosition(end), Color.Goldenrod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the dock's collision
|
||||
handle.DrawRect(new UIBox2(
|
||||
ScalePosition(rotation.Transform(new Vector2(-0.2f, -0.7f))),
|
||||
ScalePosition(rotation.Transform(new Vector2(0.2f, -0.5f)))), Color.Aquamarine);
|
||||
|
||||
// Draw the dock itself
|
||||
handle.DrawRect(new UIBox2(
|
||||
ScalePosition(rotation.Transform(new Vector2(-0.5f, 0.5f))),
|
||||
ScalePosition(rotation.Transform(new Vector2(0.5f, -0.5f)))), Color.Green);
|
||||
|
||||
// Draw nearby grids
|
||||
var worldPos = gridXform.WorldMatrix.Transform(xform.LocalPosition);
|
||||
var gridInvMatrix = gridXform.InvWorldMatrix;
|
||||
Matrix3.Multiply(in gridInvMatrix, in matrix, out var invMatrix);
|
||||
|
||||
// TODO: Getting some overdraw so need to fix that.
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(gridXform.MapID,
|
||||
new Box2(worldPos - _range, worldPos + _range)))
|
||||
{
|
||||
if (grid.GridEntityId == GridEntity) continue;
|
||||
|
||||
// Draw the fixtures before drawing any docks in range.
|
||||
if (!_entManager.TryGetComponent<FixturesComponent>(grid.GridEntityId, out var gridFixtures)) continue;
|
||||
|
||||
var gridMatrix = grid.WorldMatrix;
|
||||
|
||||
Matrix3.Multiply(in gridMatrix, in invMatrix, out var matty);
|
||||
|
||||
foreach (var (_, fixture) in gridFixtures.Fixtures)
|
||||
{
|
||||
var poly = (PolygonShape) fixture.Shape;
|
||||
|
||||
for (var i = 0; i < poly.VertexCount; i++)
|
||||
{
|
||||
// This is because the same line might be on different fixtures so we don't want to draw it twice.
|
||||
var startPos = poly.Vertices[i];
|
||||
var endPos = poly.Vertices[(i + 1) % poly.VertexCount];
|
||||
|
||||
var start = matty.Transform(startPos);
|
||||
var end = matty.Transform(endPos);
|
||||
|
||||
var startOut = start.LengthSquared > _rangeSquared;
|
||||
var endOut = end.LengthSquared > _rangeSquared;
|
||||
|
||||
// We need to draw to the radar border so we'll cap the range,
|
||||
// but if none of the verts are in range then just leave it.
|
||||
if (startOut && endOut)
|
||||
continue;
|
||||
|
||||
start.Y = -start.Y;
|
||||
end.Y = -end.Y;
|
||||
|
||||
// If start is outside we draw capped from end to start
|
||||
if (startOut)
|
||||
{
|
||||
// It's called Jobseeker now.
|
||||
if (!MathHelper.TryGetIntersecting(start, end, _range, out var newStart)) continue;
|
||||
start = newStart.Value;
|
||||
}
|
||||
// otherwise vice versa
|
||||
else if (endOut)
|
||||
{
|
||||
if (!MathHelper.TryGetIntersecting(end, start, _range, out var newEnd)) continue;
|
||||
end = newEnd.Value;
|
||||
}
|
||||
|
||||
handle.DrawLine(ScalePosition(start), ScalePosition(end), Color.Aquamarine);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw any docks on that grid
|
||||
if (Docks.TryGetValue(grid.GridEntityId, out var gridDocks))
|
||||
{
|
||||
foreach (var dock in gridDocks)
|
||||
{
|
||||
var position = matty.Transform(dock.Coordinates.Position);
|
||||
|
||||
if (position.Length > _range - 0.8f) continue;
|
||||
|
||||
var otherDockRotation = Matrix3.CreateRotation(dock.Angle);
|
||||
|
||||
// Draw the dock's collision
|
||||
var verts = new[]
|
||||
{
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(-0.2f, -0.7f))),
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(0.2f, -0.7f))),
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(0.2f, -0.5f))),
|
||||
matty.Transform(dock.Coordinates.Position +
|
||||
otherDockRotation.Transform(new Vector2(-0.2f, -0.5f))),
|
||||
};
|
||||
|
||||
for (var i = 0; i < verts.Length; i++)
|
||||
{
|
||||
var vert = verts[i];
|
||||
vert.Y = -vert.Y;
|
||||
verts[i] = ScalePosition(vert);
|
||||
}
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Turquoise);
|
||||
|
||||
// Draw the dock itself
|
||||
verts = new[]
|
||||
{
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f)),
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f)),
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f)),
|
||||
matty.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f)),
|
||||
};
|
||||
|
||||
for (var i = 0; i < verts.Length; i++)
|
||||
{
|
||||
var vert = verts[i];
|
||||
vert.Y = -vert.Y;
|
||||
verts[i] = ScalePosition(vert);
|
||||
}
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Green);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Vector2 ScalePosition(Vector2 value)
|
||||
{
|
||||
return value * MinimapScale + MidPoint;
|
||||
}
|
||||
}
|
||||
6
Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
Normal file
6
Content.Client/Shuttles/UI/RadarConsoleWindow.xaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<userInterface:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:userInterface="clr-namespace:Content.Client.UserInterface"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
|
||||
Title="{Loc 'radar-console-window-title'}">
|
||||
<ui:RadarControl Name="RadarScreen"/>
|
||||
</userInterface:FancyWindow>
|
||||
22
Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
Normal file
22
Content.Client/Shuttles/UI/RadarConsoleWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
using Content.Client.Computer;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RadarConsoleWindow : FancyWindow,
|
||||
IComputerWindow<RadarConsoleBoundInterfaceState>
|
||||
{
|
||||
public RadarConsoleWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void UpdateState(RadarConsoleBoundInterfaceState scc)
|
||||
{
|
||||
RadarScreen.UpdateState(scc);
|
||||
}
|
||||
}
|
||||
348
Content.Client/Shuttles/UI/RadarControl.cs
Normal file
348
Content.Client/Shuttles/UI/RadarControl.cs
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Displays nearby grids inside of a control.
|
||||
/// </summary>
|
||||
public sealed class RadarControl : Control
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private const float ScrollSensitivity = 8f;
|
||||
|
||||
private const int MinimapRadius = 384;
|
||||
private const int MinimapMargin = 4;
|
||||
private const float GridLinesDistance = 32f;
|
||||
|
||||
/// <summary>
|
||||
/// Entity used to transform all of the radar objects.
|
||||
/// </summary>
|
||||
private EntityUid? _entity;
|
||||
|
||||
private float _radarMinRange = 64f;
|
||||
private float _radarMaxRange = 256f;
|
||||
public float RadarRange { get; private set; } = 256f;
|
||||
|
||||
private int MidPoint => SizeFull / 2;
|
||||
private int SizeFull => (int) ((MinimapRadius + MinimapMargin) * 2 * UIScale);
|
||||
private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
|
||||
private float MinimapScale => RadarRange != 0 ? ScaledMinimapRadius / RadarRange : 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Shows a label on each radar object.
|
||||
/// </summary>
|
||||
private Dictionary<EntityUid, Control> _iffControls = new();
|
||||
|
||||
private Dictionary<EntityUid, Dictionary<EntityUid, DockingInterfaceState>> _docks = new();
|
||||
|
||||
public bool ShowIFF { get; set; } = true;
|
||||
public bool ShowDocks { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Currently hovered docked to show on the map.
|
||||
/// </summary>
|
||||
public EntityUid? HighlightedDock;
|
||||
|
||||
public Action<float>? OnRadarRangeChanged;
|
||||
|
||||
public RadarControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
MinSize = (SizeFull, SizeFull);
|
||||
}
|
||||
|
||||
public void UpdateState(RadarConsoleBoundInterfaceState ls)
|
||||
{
|
||||
_radarMaxRange = ls.MaxRange;
|
||||
|
||||
if (_radarMaxRange < RadarRange)
|
||||
{
|
||||
RadarRange = _radarMaxRange;
|
||||
OnRadarRangeChanged?.Invoke(RadarRange);
|
||||
}
|
||||
|
||||
if (_radarMaxRange < _radarMinRange)
|
||||
_radarMinRange = _radarMaxRange;
|
||||
|
||||
_entity = ls.Entity;
|
||||
_docks.Clear();
|
||||
|
||||
foreach (var state in ls.Docks)
|
||||
{
|
||||
var coordinates = state.Coordinates;
|
||||
var grid = _docks.GetOrNew(coordinates.EntityId);
|
||||
grid.Add(state.Entity, state);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void MouseWheel(GUIMouseWheelEventArgs args)
|
||||
{
|
||||
base.MouseWheel(args);
|
||||
AddRadarRange(-args.Delta.Y * ScrollSensitivity);
|
||||
}
|
||||
|
||||
public void AddRadarRange(float value)
|
||||
{
|
||||
var oldValue = RadarRange;
|
||||
RadarRange = MathF.Max(0f, MathF.Max(_radarMinRange, MathF.Min(RadarRange + value, _radarMaxRange)));
|
||||
|
||||
if (oldValue.Equals(RadarRange)) return;
|
||||
|
||||
OnRadarRangeChanged?.Invoke(RadarRange);
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
var fakeAA = new Color(0.08f, 0.08f, 0.08f);
|
||||
|
||||
handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
|
||||
handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
|
||||
|
||||
// No data
|
||||
if (_entity == null)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var gridLines = new Color(0.08f, 0.08f, 0.08f);
|
||||
var gridLinesRadial = 8;
|
||||
var gridLinesEquatorial = (int) Math.Floor(RadarRange / GridLinesDistance);
|
||||
|
||||
for (var i = 1; i < gridLinesEquatorial + 1; i++)
|
||||
{
|
||||
handle.DrawCircle((MidPoint, MidPoint), GridLinesDistance * MinimapScale * i, gridLines, false);
|
||||
}
|
||||
|
||||
for (var i = 0; i < gridLinesRadial; i++)
|
||||
{
|
||||
Angle angle = (Math.PI / gridLinesRadial) * i;
|
||||
var aExtent = angle.ToVec() * ScaledMinimapRadius;
|
||||
handle.DrawLine((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
|
||||
}
|
||||
|
||||
var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>();
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
var fixturesQuery = _entManager.GetEntityQuery<FixturesComponent>();
|
||||
var bodyQuery = _entManager.GetEntityQuery<PhysicsComponent>();
|
||||
var xform = xformQuery.GetComponent(_entity.Value);
|
||||
var mapPosition = xform.MapPosition;
|
||||
|
||||
if (mapPosition.MapId == MapId.Nullspace)
|
||||
{
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Can also use ourGridBody.LocalCenter
|
||||
var offset = xform.Coordinates.Position;
|
||||
var offsetMatrix = Matrix3.CreateTranslation(-offset);
|
||||
Matrix3 matrix;
|
||||
|
||||
// Draw our grid in detail
|
||||
var ourGridId = xform.GridID;
|
||||
if (ourGridId != GridId.Invalid)
|
||||
{
|
||||
matrix = xform.InvWorldMatrix;
|
||||
var ourGridFixtures = fixturesQuery.GetComponent(ourGridId);
|
||||
// Draw our grid; use non-filled boxes so it doesn't look awful.
|
||||
DrawGrid(handle, offsetMatrix, ourGridFixtures, Color.Yellow);
|
||||
|
||||
DrawDocks(handle, xform.GridEntityId, offsetMatrix);
|
||||
}
|
||||
else
|
||||
{
|
||||
matrix = Matrix3.CreateTranslation(-offset);
|
||||
}
|
||||
|
||||
var invertedPosition = xform.Coordinates.Position - offset;
|
||||
invertedPosition.Y = -invertedPosition.Y;
|
||||
// Don't need to transform the InvWorldMatrix again as it's already offset to its position.
|
||||
|
||||
// Draw radar position on the station
|
||||
handle.DrawCircle(ScalePosition(invertedPosition), 5f, Color.Lime);
|
||||
|
||||
var shown = new HashSet<EntityUid>();
|
||||
|
||||
// Draw other grids... differently
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(mapPosition.MapId,
|
||||
new Box2(mapPosition.Position - RadarRange, mapPosition.Position + RadarRange)))
|
||||
{
|
||||
if (grid.Index == ourGridId) continue;
|
||||
|
||||
var gridBody = bodyQuery.GetComponent(grid.GridEntityId);
|
||||
if (gridBody.Mass < 10f)
|
||||
{
|
||||
ClearLabel(grid.GridEntityId);
|
||||
continue;
|
||||
}
|
||||
|
||||
shown.Add(grid.GridEntityId);
|
||||
var name = metaQuery.GetComponent(grid.GridEntityId).EntityName;
|
||||
|
||||
if (name == string.Empty)
|
||||
name = Loc.GetString("shuttle-console-unknown");
|
||||
|
||||
var gridXform = xformQuery.GetComponent(grid.GridEntityId);
|
||||
var gridFixtures = fixturesQuery.GetComponent(grid.GridEntityId);
|
||||
var gridMatrix = gridXform.WorldMatrix;
|
||||
Matrix3.Multiply(in gridMatrix, in matrix, out var matty);
|
||||
|
||||
if (ShowIFF)
|
||||
{
|
||||
if (!_iffControls.TryGetValue(grid.GridEntityId, out var control))
|
||||
{
|
||||
var label = new Label()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
};
|
||||
|
||||
control = new PanelContainer()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
VerticalAlignment = VAlignment.Top,
|
||||
Children = { label },
|
||||
StyleClasses = { StyleNano.StyleClassTooltipPanel },
|
||||
};
|
||||
|
||||
_iffControls[grid.GridEntityId] = control;
|
||||
AddChild(control);
|
||||
}
|
||||
|
||||
var gridCentre = matty.Transform(gridBody.LocalCenter);
|
||||
gridCentre.Y = -gridCentre.Y;
|
||||
|
||||
// TODO: When we get IFF or whatever we can show controls at a further distance; for now
|
||||
// we don't do that because it would immediately reveal nukies.
|
||||
if (gridCentre.Length < RadarRange)
|
||||
{
|
||||
control.Visible = true;
|
||||
var label = (Label) control.GetChild(0);
|
||||
label.Text = Loc.GetString("shuttle-console-iff-label", ("name", name), ("distance", $"{gridCentre.Length:0.0}"));
|
||||
LayoutContainer.SetPosition(control, ScalePosition(gridCentre) / UIScale);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.Visible = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearLabel(grid.GridEntityId);
|
||||
}
|
||||
|
||||
// Detailed view
|
||||
DrawGrid(handle, matty, gridFixtures, Color.Aquamarine);
|
||||
|
||||
DrawDocks(handle, grid.GridEntityId, matty);
|
||||
}
|
||||
|
||||
foreach (var (ent, _) in _iffControls)
|
||||
{
|
||||
if (shown.Contains(ent)) continue;
|
||||
ClearLabel(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
foreach (var (_, label) in _iffControls)
|
||||
{
|
||||
label.Dispose();
|
||||
}
|
||||
|
||||
_iffControls.Clear();
|
||||
}
|
||||
|
||||
private void ClearLabel(EntityUid uid)
|
||||
{
|
||||
if (!_iffControls.TryGetValue(uid, out var label)) return;
|
||||
label.Dispose();
|
||||
_iffControls.Remove(uid);
|
||||
}
|
||||
|
||||
private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3 matrix)
|
||||
{
|
||||
if (!ShowDocks) return;
|
||||
|
||||
const float DockScale = 1.2f;
|
||||
|
||||
if (_docks.TryGetValue(uid, out var docks))
|
||||
{
|
||||
foreach (var (ent, state) in docks)
|
||||
{
|
||||
var position = state.Coordinates.Position;
|
||||
var uiPosition = matrix.Transform(position);
|
||||
|
||||
if (uiPosition.Length > RadarRange - DockScale) continue;
|
||||
|
||||
var color = HighlightedDock == ent ? Color.Magenta : Color.DarkViolet;
|
||||
|
||||
uiPosition.Y = -uiPosition.Y;
|
||||
|
||||
var verts = new[]
|
||||
{
|
||||
matrix.Transform(position + new Vector2(-DockScale, -DockScale)),
|
||||
matrix.Transform(position + new Vector2(DockScale, -DockScale)),
|
||||
matrix.Transform(position + new Vector2(DockScale, DockScale)),
|
||||
matrix.Transform(position + new Vector2(-DockScale, DockScale)),
|
||||
};
|
||||
|
||||
for (var i = 0; i < verts.Length; i++)
|
||||
{
|
||||
var vert = verts[i];
|
||||
vert.Y = -vert.Y;
|
||||
verts[i] = ScalePosition(vert);
|
||||
}
|
||||
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGrid(DrawingHandleScreen handle, Matrix3 matrix, FixturesComponent component, Color color)
|
||||
{
|
||||
foreach (var (_, fixture) in component.Fixtures)
|
||||
{
|
||||
// If the fixture has any points out of range we won't draw any of it.
|
||||
var invalid = false;
|
||||
var poly = (PolygonShape) fixture.Shape;
|
||||
var verts = new Vector2[poly.VertexCount + 1];
|
||||
|
||||
for (var i = 0; i < poly.VertexCount; i++)
|
||||
{
|
||||
var vert = matrix.Transform(poly.Vertices[i]);
|
||||
|
||||
if (vert.Length > RadarRange)
|
||||
{
|
||||
invalid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
vert.Y = -vert.Y;
|
||||
verts[i] = ScalePosition(vert);
|
||||
}
|
||||
|
||||
if (invalid) continue;
|
||||
|
||||
// Closed list
|
||||
verts[poly.VertexCount] = verts[0];
|
||||
handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 ScalePosition(Vector2 value)
|
||||
{
|
||||
return value * MinimapScale + MidPoint;
|
||||
}
|
||||
}
|
||||
81
Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
Normal file
81
Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<userInterface:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:userInterface="clr-namespace:Content.Client.UserInterface"
|
||||
xmlns:ui="clr-namespace:Content.Client.Shuttles.UI"
|
||||
Title="{Loc 'shuttle-console-window-title'}">
|
||||
<GridContainer Columns="3"
|
||||
HorizontalAlignment="Stretch">
|
||||
<BoxContainer Name="LeftDisplay"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Left"
|
||||
MinWidth="256"
|
||||
Align="Center"
|
||||
Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="BorderedWindowPanel">
|
||||
<BoxContainer Name="DockPorts"
|
||||
Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<PanelContainer>
|
||||
<ui:RadarControl Name="RadarScreen"
|
||||
MouseFilter="Stop"/>
|
||||
<ui:DockingControl Name="DockingScreen"
|
||||
Visible="False"
|
||||
MouseFilter="Stop"/>
|
||||
</PanelContainer>
|
||||
<BoxContainer Name="RightDisplay"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Left"
|
||||
MinWidth="256"
|
||||
Align="Center"
|
||||
Orientation="Vertical">
|
||||
<PanelContainer Name="ReadonlyDisplay"
|
||||
StyleClasses="BorderedWindowPanel">
|
||||
<GridContainer Columns="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top">
|
||||
<Label Text="{Loc 'shuttle-console-max-radar'}"/>
|
||||
<Label Name="MaxRadarRange"
|
||||
Text="0.0"
|
||||
HorizontalAlignment="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-radar'}"/>
|
||||
<Label Name="RadarRange"
|
||||
Text="0.0"
|
||||
HorizontalAlignment="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-position'}"/>
|
||||
<Label Name="GridPosition"
|
||||
Text="0.0, 0.0"
|
||||
Align="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-orientation'}"/>
|
||||
<Label Name="GridOrientation"
|
||||
Text="0.0"
|
||||
Align="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-linear-velocity'}"/>
|
||||
<Label Name="GridLinearVelocity"
|
||||
Text="0.0, 0.0"
|
||||
Align="Right"/>
|
||||
<Label Text="{Loc 'shuttle-console-angular-velocity'}"/>
|
||||
<Label Name="GridAngularVelocity"
|
||||
Text="0.0"
|
||||
Align="Right"/>
|
||||
</GridContainer>
|
||||
</PanelContainer>
|
||||
<Button Name="ShuttleModeDisplay"
|
||||
Text="{Loc 'shuttle-console-strafing'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<Button Name="IFFToggle"
|
||||
Text="{Loc 'shuttle-console-iff-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<Button Name="DockToggle"
|
||||
Text="{Loc 'shuttle-console-dock-toggle'}"
|
||||
TextAlign="Center"
|
||||
ToggleMode="True"/>
|
||||
<Button Name="UndockButton"
|
||||
Text="{Loc 'shuttle-console-undock'}"
|
||||
TextAlign="Center"
|
||||
Disabled="True"/>
|
||||
</BoxContainer>
|
||||
</GridContainer>
|
||||
</userInterface:FancyWindow>
|
||||
239
Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
Normal file
239
Content.Client/Shuttles/UI/ShuttleConsoleWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
using Content.Client.Computer;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ShuttleConsoleWindow : FancyWindow,
|
||||
IComputerWindow<ShuttleConsoleBoundInterfaceState>
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
|
||||
/// <summary>
|
||||
/// EntityUid of the open console.
|
||||
/// </summary>
|
||||
private EntityUid? _entity;
|
||||
|
||||
/// <summary>
|
||||
/// Currently selected dock button for camera.
|
||||
/// </summary>
|
||||
private BaseButton? _selectedDock;
|
||||
|
||||
/// <summary>
|
||||
/// Stored by grid entityid then by states
|
||||
/// </summary>
|
||||
private Dictionary<EntityUid, List<DockingInterfaceState>> _docks = new();
|
||||
|
||||
public Action<ShuttleMode>? ShuttleModePressed;
|
||||
public Action<EntityUid>? UndockPressed;
|
||||
public Action<EntityUid>? StartAutodockPressed;
|
||||
public Action<EntityUid>? StopAutodockPressed;
|
||||
|
||||
public ShuttleConsoleWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
OnRadarRangeChange(RadarScreen.RadarRange);
|
||||
RadarScreen.OnRadarRangeChanged += OnRadarRangeChange;
|
||||
|
||||
IFFToggle.OnToggled += OnIFFTogglePressed;
|
||||
IFFToggle.Pressed = RadarScreen.ShowIFF;
|
||||
|
||||
DockToggle.OnToggled += OnDockTogglePressed;
|
||||
DockToggle.Pressed = RadarScreen.ShowDocks;
|
||||
|
||||
ShuttleModeDisplay.OnToggled += OnShuttleModePressed;
|
||||
|
||||
UndockButton.OnPressed += OnUndockPressed;
|
||||
}
|
||||
|
||||
private void OnRadarRangeChange(float value)
|
||||
{
|
||||
RadarRange.Text = $"{value:0}";
|
||||
}
|
||||
|
||||
private void OnShuttleModePressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
ShuttleModePressed?.Invoke(obj.Button.Pressed ? ShuttleMode.Strafing : ShuttleMode.Cruise);
|
||||
}
|
||||
|
||||
private void OnIFFTogglePressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
RadarScreen.ShowIFF ^= true;
|
||||
args.Button.Pressed = RadarScreen.ShowIFF;
|
||||
}
|
||||
|
||||
private void OnDockTogglePressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
RadarScreen.ShowDocks ^= true;
|
||||
args.Button.Pressed = RadarScreen.ShowDocks;
|
||||
}
|
||||
|
||||
private void OnUndockPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (DockingScreen.ViewedDock == null) return;
|
||||
UndockPressed?.Invoke(DockingScreen.ViewedDock.Value);
|
||||
}
|
||||
|
||||
public void UpdateState(ShuttleConsoleBoundInterfaceState scc)
|
||||
{
|
||||
_entity = scc.Entity;
|
||||
UpdateDocks(scc.Docks);
|
||||
RadarScreen.UpdateState(scc);
|
||||
MaxRadarRange.Text = $"{scc.MaxRange:0}";
|
||||
ShuttleModeDisplay.Pressed = scc.Mode == ShuttleMode.Strafing;
|
||||
}
|
||||
|
||||
#region Docking
|
||||
|
||||
private void UpdateDocks(List<DockingInterfaceState> docks)
|
||||
{
|
||||
// TODO: We should check for changes so any existing highlighted doesn't delete.
|
||||
// We also need to make up some pseudonumber as well for these.
|
||||
_docks.Clear();
|
||||
|
||||
foreach (var dock in docks)
|
||||
{
|
||||
var grid = _docks.GetOrNew(dock.Coordinates.EntityId);
|
||||
grid.Add(dock);
|
||||
}
|
||||
|
||||
DockPorts.DisposeAllChildren();
|
||||
DockingScreen.Docks = _docks;
|
||||
|
||||
if (!_entManager.TryGetComponent<TransformComponent>(_entity, out var xform))
|
||||
{
|
||||
// TODO: Show Placeholder
|
||||
return;
|
||||
}
|
||||
|
||||
if (_docks.TryGetValue(xform.GridEntityId, out var gridDocks))
|
||||
{
|
||||
var index = 1;
|
||||
|
||||
foreach (var state in gridDocks)
|
||||
{
|
||||
var ent = state.Entity;
|
||||
var pressed = ent == DockingScreen.ViewedDock;
|
||||
string suffix;
|
||||
|
||||
if (state.Connected)
|
||||
{
|
||||
suffix = Loc.GetString("shuttle-console-docked", ("index", index));
|
||||
}
|
||||
else
|
||||
{
|
||||
suffix = $"{index}";
|
||||
}
|
||||
|
||||
var button = new Button()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-console-dock-button", ("suffix", suffix)),
|
||||
ToggleMode = true,
|
||||
Pressed = pressed,
|
||||
};
|
||||
|
||||
button.OnMouseEntered += args => OnDockMouseEntered(args, ent);
|
||||
button.OnMouseExited += args => OnDockMouseExited(args, ent);
|
||||
button.OnToggled += args => OnDockToggled(args, ent);
|
||||
DockPorts.AddChild(button);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDockMouseEntered(GUIMouseHoverEventArgs obj, EntityUid uid)
|
||||
{
|
||||
RadarScreen.HighlightedDock = uid;
|
||||
}
|
||||
|
||||
private void OnDockMouseExited(GUIMouseHoverEventArgs obj, EntityUid uid)
|
||||
{
|
||||
RadarScreen.HighlightedDock = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a docking camera instead of radar screen.
|
||||
/// </summary>
|
||||
private void OnDockToggled(BaseButton.ButtonEventArgs obj, EntityUid ent)
|
||||
{
|
||||
if (_selectedDock != null)
|
||||
{
|
||||
_selectedDock.Pressed = false;
|
||||
_selectedDock = null;
|
||||
}
|
||||
|
||||
if (!obj.Button.Pressed)
|
||||
{
|
||||
if (DockingScreen.ViewedDock != null)
|
||||
{
|
||||
StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
|
||||
DockingScreen.ViewedDock = null;
|
||||
}
|
||||
|
||||
UndockButton.Disabled = true;
|
||||
DockingScreen.Visible = false;
|
||||
RadarScreen.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// DebugTools.Assert(DockingScreen.ViewedDock == null);
|
||||
_entManager.TryGetComponent<TransformComponent>(_entity, out var xform);
|
||||
|
||||
UndockButton.Disabled = false;
|
||||
RadarScreen.Visible = false;
|
||||
DockingScreen.Visible = true;
|
||||
DockingScreen.ViewedDock = ent;
|
||||
StartAutodockPressed?.Invoke(ent);
|
||||
DockingScreen.GridEntity = xform?.GridEntityId;
|
||||
_selectedDock = obj.Button;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
base.Close();
|
||||
if (DockingScreen.ViewedDock != null)
|
||||
{
|
||||
StopAutodockPressed?.Invoke(DockingScreen.ViewedDock.Value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (!_entManager.TryGetComponent<TransformComponent>(_entity, out var entXform) ||
|
||||
!_entManager.TryGetComponent<PhysicsComponent>(entXform.GridEntityId, out var gridBody) ||
|
||||
!_entManager.TryGetComponent<TransformComponent>(entXform.GridEntityId, out var gridXform))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (_, worldRot, worldMatrix) = gridXform.GetWorldPositionRotationMatrix();
|
||||
var worldPos = worldMatrix.Transform(gridBody.LocalCenter);
|
||||
|
||||
// Get the positive reduced angle.
|
||||
var displayRot = -worldRot.Reduced();
|
||||
|
||||
GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
|
||||
GridOrientation.Text = $"{displayRot.Degrees:0.0}";
|
||||
|
||||
var gridVelocity = gridBody.LinearVelocity;
|
||||
gridVelocity = displayRot.RotateVec(gridVelocity);
|
||||
// Get linear velocity relative to the console entity
|
||||
GridLinearVelocity.Text = $"{gridVelocity.X:0.0}, {gridVelocity.Y:0.0}";
|
||||
GridAngularVelocity.Text = $"{-gridBody.AngularVelocity:0.0}";
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ namespace Content.Client.SurveillanceCamera;
|
|||
|
||||
public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
{
|
||||
private readonly RemQueue<EntityUid> _toRemove = new();
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
foreach (var comp in EntityQuery<ActiveSurveillanceCameraMonitorVisualsComponent>())
|
||||
|
|
@ -24,16 +22,9 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
|||
comp.OnFinish();
|
||||
}
|
||||
|
||||
_toRemove.Add(comp.Owner);
|
||||
EntityManager.RemoveComponentDeferred<ActiveSurveillanceCameraMonitorVisualsComponent>(comp.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var uid in _toRemove)
|
||||
{
|
||||
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorVisualsComponent>(uid);
|
||||
}
|
||||
|
||||
_toRemove.List?.Clear();
|
||||
}
|
||||
|
||||
public void AddTimer(EntityUid uid, Action onFinish)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ namespace Content.Server.Administration.UI
|
|||
{
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
private readonly ChatSystem _chatSystem;
|
||||
|
||||
public AdminAnnounceEui()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_chatSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Server.Shuttles.EntitySystems;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using JetBrains.Annotations;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ namespace Content.Server.Atmos.Commands
|
|||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if(EntityUid.TryParse(args[0], out var euid))
|
||||
if (!EntityUid.TryParse(args[0], out var euid))
|
||||
{
|
||||
shell.WriteError($"Failed to parse euid '{args[0]}'.");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Shuttles.EntitySystems;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
|
|
|
|||
92
Content.Server/Atmos/Miasma/MiasmaSystem.cs
Normal file
92
Content.Server/Atmos/Miasma/MiasmaSystem.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
using Content.Shared.MobState;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Server.Body.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.Atmos.Miasma
|
||||
{
|
||||
public sealed class MiasmaSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var (rotting, perishable) in EntityQuery<RottingComponent, PerishableComponent>())
|
||||
{
|
||||
if (!perishable.Progressing)
|
||||
continue;
|
||||
|
||||
if (TryComp<TemperatureComponent>(perishable.Owner, out var temp) && temp.CurrentTemperature < 274f)
|
||||
continue;
|
||||
|
||||
perishable.DeathAccumulator += frameTime;
|
||||
if (perishable.DeathAccumulator < perishable.RotAfter.TotalSeconds)
|
||||
continue;
|
||||
|
||||
perishable.RotAccumulator += frameTime;
|
||||
if (perishable.RotAccumulator < 1f)
|
||||
continue;
|
||||
|
||||
perishable.RotAccumulator -= 1f;
|
||||
|
||||
DamageSpecifier damage = new();
|
||||
damage.DamageDict.Add("Blunt", 0.25); // Slowly accumulate enough to gib after like half an hour
|
||||
damage.DamageDict.Add("Cellular", 0.25); // Cloning rework might use this eventually
|
||||
|
||||
_damageableSystem.TryChangeDamage(perishable.Owner, damage, true, true);
|
||||
|
||||
if (!TryComp<PhysicsComponent>(perishable.Owner, out var physics))
|
||||
continue;
|
||||
// We need a way to get the mass of the mob alone without armor etc in the future
|
||||
|
||||
var tileMix = _atmosphereSystem.GetTileMixture(Transform(perishable.Owner).Coordinates);
|
||||
if (tileMix != null)
|
||||
tileMix.AdjustMoles(Gas.Miasma, perishable.MolsPerSecondPerUnitMass * physics.FixturesMass);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PerishableComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<PerishableComponent, BeingGibbedEvent>(OnGibbed);
|
||||
SubscribeLocalEvent<AntiRottingContainerComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
|
||||
SubscribeLocalEvent<AntiRottingContainerComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(EntityUid uid, PerishableComponent component, MobStateChangedEvent args)
|
||||
{
|
||||
if (args.Component.IsDead())
|
||||
EnsureComp<RottingComponent>(uid);
|
||||
}
|
||||
|
||||
private void OnGibbed(EntityUid uid, PerishableComponent component, BeingGibbedEvent args)
|
||||
{
|
||||
if (!TryComp<PhysicsComponent>(uid, out var physics))
|
||||
return;
|
||||
|
||||
var molsToDump = (component.MolsPerSecondPerUnitMass * physics.FixturesMass) * component.DeathAccumulator;
|
||||
var tileMix = _atmosphereSystem.GetTileMixture(Transform(uid).Coordinates);
|
||||
if (tileMix != null)
|
||||
tileMix.AdjustMoles(Gas.Miasma, molsToDump);
|
||||
}
|
||||
|
||||
private void OnEntInserted(EntityUid uid, AntiRottingContainerComponent component, EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (TryComp<PerishableComponent>(args.Entity, out var perishable))
|
||||
perishable.Progressing = false;
|
||||
}
|
||||
|
||||
private void OnEntRemoved(EntityUid uid, AntiRottingContainerComponent component, EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (TryComp<PerishableComponent>(args.Entity, out var perishable))
|
||||
perishable.Progressing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Content.Server/Atmos/Miasma/NoRotContainerComponent.cs
Normal file
9
Content.Server/Atmos/Miasma/NoRotContainerComponent.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Content.Server.Atmos.Miasma
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// Entities inside this container will not rot.
|
||||
/// </summary>
|
||||
public sealed class AntiRottingContainerComponent : Component
|
||||
{}
|
||||
}
|
||||
42
Content.Server/Atmos/Miasma/PerishableComponent.cs
Normal file
42
Content.Server/Atmos/Miasma/PerishableComponent.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
namespace Content.Server.Atmos.Miasma
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// This makes mobs eventually start rotting when they die.
|
||||
/// It may be expanded to food at some point, but it's just for mobs right now.
|
||||
/// </summary>
|
||||
public sealed class PerishableComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this progressing?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Progressing = true;
|
||||
|
||||
/// <summary>
|
||||
/// How long this creature has been dead.
|
||||
/// </summary>
|
||||
[DataField("deathAccumulator")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float DeathAccumulator = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// When DeathAccumulator is greater than this, start rotting.
|
||||
/// </summary>
|
||||
public TimeSpan RotAfter = TimeSpan.FromMinutes(3);
|
||||
|
||||
/// <summary>
|
||||
/// Gasses are released every second.
|
||||
/// </summary>
|
||||
[DataField("rotAccumulator")]
|
||||
public float RotAccumulator = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// How many moles of gas released per second, adjusted for mass.
|
||||
/// Humans have a mass of 70. I am aiming for ten mols a minute, so
|
||||
/// 1/6 of a minute, divided by 70 as a baseline.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MolsPerSecondPerUnitMass = 0.0025f;
|
||||
}
|
||||
}
|
||||
9
Content.Server/Atmos/Miasma/RottingComponent.cs
Normal file
9
Content.Server/Atmos/Miasma/RottingComponent.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Content.Server.Atmos.Miasma
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// Tracking component for stuff that has started to rot.
|
||||
/// </summary>
|
||||
public sealed class RottingComponent : Component
|
||||
{}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Server.Disease;
|
||||
using Content.Shared.Disease.Components;
|
||||
using Robust.Shared.Random;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Chemistry.ReagentEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// Default metabolism for medicine reagents.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class ChemCauseRandomDisease : ReagentEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// A disease to choose from.
|
||||
/// </summary>
|
||||
[DataField("diseases", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<string> Diseases = default!;
|
||||
|
||||
public override void Effect(ReagentEffectArgs args)
|
||||
{
|
||||
if (args.EntityManager.TryGetComponent<DiseasedComponent>(args.SolutionEntity, out var diseased))
|
||||
return;
|
||||
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
|
||||
EntitySystem.Get<DiseaseSystem>().TryAddDisease(args.SolutionEntity, random.Pick(Diseases));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
using Content.Server.Actions.Events;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.CombatMode.Disarm;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Weapon.Melee;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Stunnable;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Content.Server.CombatMode
|
||||
{
|
||||
|
|
@ -38,9 +42,17 @@ namespace Content.Server.CombatMode
|
|||
if (!_actionBlockerSystem.CanAttack(args.Performer))
|
||||
return;
|
||||
|
||||
if (TryComp<HandsComponent>(args.Performer, out var hands)
|
||||
&& hands.ActiveHand != null
|
||||
&& !hands.ActiveHand.IsEmpty)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("disarm-action-free-hand"), args.Performer, Filter.Entities(args.Performer));
|
||||
return;
|
||||
}
|
||||
|
||||
EntityUid? inTargetHand = null;
|
||||
|
||||
if (EntityManager.TryGetComponent<HandsComponent>(args.Target, out HandsComponent? targetHandsComponent)
|
||||
if (TryComp<HandsComponent>(args.Target, out HandsComponent? targetHandsComponent)
|
||||
&& targetHandsComponent.ActiveHand != null
|
||||
&& !targetHandsComponent.ActiveHand.IsEmpty)
|
||||
{
|
||||
|
|
@ -64,8 +76,8 @@ namespace Content.Server.CombatMode
|
|||
var filterOther = filterAll.RemoveWhereAttachedEntity(e => e == args.Performer);
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (_random.Prob(component.DisarmFailChance))
|
||||
var chance = CalculateDisarmChance(args.Performer, args.Target, inTargetHand, component);
|
||||
if (_random.Prob(chance))
|
||||
{
|
||||
SoundSystem.Play(component.DisarmFailSound.GetSound(), Filter.Pvs(args.Performer), args.Performer, AudioHelpers.WithVariation(0.025f));
|
||||
|
||||
|
|
@ -89,8 +101,40 @@ namespace Content.Server.CombatMode
|
|||
SoundSystem.Play(component.DisarmSuccessSound.GetSound(), filterAll, args.Performer, AudioHelpers.WithVariation(0.025f));
|
||||
_adminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(args.Performer):user} used disarm on {ToPrettyString(args.Target):target}");
|
||||
|
||||
var eventArgs = new DisarmedEvent() { Target = args.Target, Source = args.Performer, PushProbability = component.DisarmPushChance };
|
||||
var eventArgs = new DisarmedEvent() { Target = args.Target, Source = args.Performer, PushProbability = chance };
|
||||
RaiseLocalEvent(args.Target, eventArgs);
|
||||
}
|
||||
|
||||
|
||||
private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, SharedCombatModeComponent disarmerComp)
|
||||
{
|
||||
float healthMod = 0;
|
||||
if (TryComp<DamageableComponent>(disarmer, out var disarmerDamage) && TryComp<DamageableComponent>(disarmed, out var disarmedDamage))
|
||||
{
|
||||
// I wanted this to consider their mob state thresholds too but I'm not touching that shitcode after having a go at this.
|
||||
healthMod = (((float) disarmedDamage.TotalDamage - (float) disarmerDamage.TotalDamage) / 200); // Ex. You have 0 damage, they have 90, you get a 45% chance increase
|
||||
}
|
||||
|
||||
float massMod = 0;
|
||||
|
||||
if (TryComp<PhysicsComponent>(disarmer, out var disarmerPhysics) && TryComp<PhysicsComponent>(disarmed, out var disarmedPhysics))
|
||||
{
|
||||
if (disarmerPhysics.FixturesMass != 0) // yeah this will never happen but let's not kill the server if it does
|
||||
massMod = (((disarmedPhysics.FixturesMass / disarmerPhysics.FixturesMass - 1 ) / 2)); // Ex, you weigh 120, they weigh 70, you get a 29% bonus
|
||||
}
|
||||
|
||||
float chance = (disarmerComp.BaseDisarmFailChance - healthMod - massMod);
|
||||
if (HasComp<SlowedDownComponent>(disarmer)) // might need to revisit this part after stamina damage, right now this is basically "pre-stun"
|
||||
chance += 0.35f;
|
||||
if (HasComp<SlowedDownComponent>(disarmed))
|
||||
chance -= 0.35f;
|
||||
|
||||
if (inTargetHand != null && TryComp<DisarmMalusComponent>(inTargetHand, out var malus))
|
||||
{
|
||||
chance += malus.Malus;
|
||||
}
|
||||
|
||||
return Math.Clamp(chance, 0f, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
Content.Server/CombatMode/Disarm/DisarmMalusComponent.cs
Normal file
16
Content.Server/CombatMode/Disarm/DisarmMalusComponent.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace Content.Server.CombatMode.Disarm
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a malus to disarm attempts against this item.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DisarmMalusComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// So, disarm chances are a % chance represented as a value between 0 and 1.
|
||||
/// This default would be a 30% penalty to that.
|
||||
/// </summary>
|
||||
[DataField("malus")]
|
||||
public float Malus = 0.3f;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ namespace Content.Server.Construction
|
|||
|
||||
private void AddDeconstructVerb(EntityUid uid, ConstructionComponent component, GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
|
||||
return;
|
||||
|
||||
if (component.TargetNode == component.DeconstructionNode ||
|
||||
|
|
|
|||
|
|
@ -245,9 +245,7 @@ namespace Content.Server.Cuffs.Components
|
|||
SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner);
|
||||
|
||||
Container.ForceRemove(cuffsToRemove.Value);
|
||||
var transform = _entMan.GetComponent<TransformComponent>(cuffsToRemove.Value);
|
||||
transform.AttachToGridOrMap();
|
||||
transform.WorldPosition = _entMan.GetComponent<TransformComponent>(Owner).WorldPosition;
|
||||
_entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, cuffsToRemove.Value);
|
||||
|
||||
if (cuff.BreakOnRemove)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Content.Shared.Popups;
|
|||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
|
||||
namespace Content.Server.Cuffs
|
||||
{
|
||||
|
|
@ -23,14 +24,13 @@ namespace Content.Server.Cuffs
|
|||
|
||||
SubscribeLocalEvent<HandCountChangedEvent>(OnHandCountChanged);
|
||||
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
|
||||
|
||||
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
|
||||
}
|
||||
|
||||
private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
|
||||
{
|
||||
// Can the user access the cuffs, and is there even anything to uncuff?
|
||||
if (!args.CanAccess || component.CuffedHandCount == 0)
|
||||
if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
|
||||
return;
|
||||
|
||||
// We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// Allows the enity to be infected with diseases.
|
||||
/// Allows the entity to be infected with diseases.
|
||||
/// Please use only on mobs.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseCarrierComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -15,22 +16,33 @@ namespace Content.Server.Disease.Components
|
|||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> Diseases = new();
|
||||
|
||||
/// <summary>
|
||||
/// The carrier's resistance to disease
|
||||
/// </summary>
|
||||
[DataField("diseaseResist")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float DiseaseResist = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Diseases the carrier has had, used for immunity.
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> PastDiseases = new();
|
||||
|
||||
/// <summary>
|
||||
/// All the diseases the carrier has or has had.
|
||||
/// Checked against when trying to add a disease
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> AllDiseases => PastDiseases.Concat(Diseases).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// A list of diseases which the entity does not
|
||||
/// exhibit direct symptoms from. They still transmit
|
||||
/// these diseases, just without symptoms.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("carrierDiseases", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<DiseasePrototype>))]
|
||||
public HashSet<string>? CarrierDiseases;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,17 +96,23 @@ namespace Content.Server.Disease
|
|||
for (var i = 0; i < carrierComp.Diseases.Count; i++) //this is a for-loop so that it doesn't break when new diseases are added
|
||||
{
|
||||
var disease = carrierComp.Diseases[i];
|
||||
|
||||
var args = new DiseaseEffectArgs(carrierComp.Owner, disease, EntityManager);
|
||||
disease.Accumulator += frameTime;
|
||||
if (disease.Accumulator >= disease.TickTime)
|
||||
|
||||
if (disease.Accumulator < disease.TickTime) continue;
|
||||
|
||||
// if the disease is on the silent disease list, don't do effects
|
||||
var doEffects = carrierComp.CarrierDiseases?.Contains(disease.ID) != true;
|
||||
var args = new DiseaseEffectArgs(carrierComp.Owner, disease, EntityManager);
|
||||
disease.Accumulator -= disease.TickTime;
|
||||
|
||||
foreach (var cure in disease.Cures)
|
||||
{
|
||||
if (cure.Cure(args))
|
||||
CureDisease(carrierComp, disease);
|
||||
}
|
||||
|
||||
if (doEffects)
|
||||
{
|
||||
disease.Accumulator -= disease.TickTime;
|
||||
foreach (var cure in disease.Cures)
|
||||
{
|
||||
if (cure.Cure(args))
|
||||
CureDisease(carrierComp, disease);
|
||||
}
|
||||
foreach (var effect in disease.Effects)
|
||||
{
|
||||
if (_random.Prob(effect.Probability))
|
||||
|
|
@ -383,6 +389,14 @@ namespace Content.Server.Disease
|
|||
TryAddDisease(carrier.Owner, disease, carrier);
|
||||
}
|
||||
|
||||
public void TryInfect(DiseaseCarrierComponent carrier, string? disease, float chance = 0.7f, bool forced = false)
|
||||
{
|
||||
if (disease == null || !_prototypeManager.TryIndex<DiseasePrototype>(disease, out var d))
|
||||
return;
|
||||
|
||||
TryInfect(carrier, d, chance, forced);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sneeze/cough popup if applicable
|
||||
/// and then tries to infect anyone in range
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
|||
foreach (var other in PhysicsSystem.GetCollidingEntities(body))
|
||||
{
|
||||
if (Tags.HasTag(other.Owner, "DoorBumpOpener") &&
|
||||
TryOpen(component.Owner, component, other.Owner)) break;
|
||||
TryOpen(component.Owner, component, other.Owner, false, quiet: true)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
108
Content.Server/Dragon/DragonComponent.cs
Normal file
108
Content.Server/Dragon/DragonComponent.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.Containers;
|
||||
using System.Threading;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Dragon
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class DragonComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The chemical ID injected upon devouring
|
||||
/// </summary>
|
||||
[DataField("devourChemical", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentPrototype>))]
|
||||
public string DevourChem = "Ichor";
|
||||
|
||||
/// <summary>
|
||||
/// The amount of ichor injected per devour
|
||||
/// </summary>
|
||||
[DataField("devourHealRate")]
|
||||
public float DevourHealRate = 15f;
|
||||
|
||||
[DataField("devourActionId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityTargetActionPrototype>))]
|
||||
public string DevourActionId = "DragonDevour";
|
||||
|
||||
[DataField("devourAction")]
|
||||
public EntityTargetAction? DevourAction;
|
||||
|
||||
[DataField("spawnActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
|
||||
public string SpawnActionId = "DragonSpawn";
|
||||
|
||||
[DataField("spawnAction")]
|
||||
public InstantAction? SpawnAction;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time it takes to devour something
|
||||
/// <remarks>
|
||||
/// NOTE: original intended design was to increase this proportionally with damage thresholds, but those proved quite difficult to get consistently. right now it devours the structure at a fixed timer.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
[DataField("structureDevourTime")]
|
||||
public float StructureDevourTime = 10f;
|
||||
|
||||
[DataField("devourTime")]
|
||||
public float DevourTime = 2f;
|
||||
|
||||
[DataField("spawnCount")] public int SpawnsLeft = 2;
|
||||
|
||||
[DataField("maxSpawnCount")] public int MaxSpawns = 2;
|
||||
|
||||
[DataField("spawnProto", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string? SpawnPrototype = "MobCarpDragon";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("soundDeath")]
|
||||
public SoundSpecifier? SoundDeath = new SoundPathSpecifier("/Audio/Animals/space_dragon_roar.ogg");
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("soundDevour")]
|
||||
public SoundSpecifier? SoundDevour = new SoundPathSpecifier("/Audio/Effects/demon_consume.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-3f),
|
||||
};
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("soundStructureDevour")]
|
||||
public SoundSpecifier? SoundStructureDevour = new SoundPathSpecifier("/Audio/Machines/airlock_creaking.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-3f),
|
||||
};
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("soundRoar")]
|
||||
public SoundSpecifier? SoundRoar =
|
||||
new SoundPathSpecifier("/Audio/Animals/space_dragon_roar.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-3f),
|
||||
};
|
||||
|
||||
public CancellationTokenSource? CancelToken;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("devourWhitelist")]
|
||||
public EntityWhitelist? DevourWhitelist = new()
|
||||
{
|
||||
Components = new string[]
|
||||
{
|
||||
"Door",
|
||||
"MobState",
|
||||
},
|
||||
Tags = new List<string>()
|
||||
{
|
||||
"Wall",
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Where the entities go when dragon devours them, empties when the dragon is butchered.
|
||||
/// </summary>
|
||||
public Container DragonStomach = default!;
|
||||
}
|
||||
|
||||
public sealed class DragonDevourActionEvent : EntityTargetActionEvent {}
|
||||
|
||||
public sealed class DragonSpawnActionEvent : InstantActionEvent {}
|
||||
}
|
||||
209
Content.Server/Dragon/DragonSystem.cs
Normal file
209
Content.Server/Dragon/DragonSystem.cs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
using Content.Server.Body.Systems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.CharacterAppearance.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.MobState;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using System.Threading;
|
||||
using Content.Shared.MobState.State;
|
||||
using Content.Shared.Doors.Components;
|
||||
|
||||
namespace Content.Server.Dragon
|
||||
{
|
||||
public sealed class DragonSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DragonComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<DragonComponent, DragonDevourComplete>(OnDragonDevourComplete);
|
||||
SubscribeLocalEvent<DragonComponent, DragonDevourActionEvent>(OnDevourAction);
|
||||
SubscribeLocalEvent<DragonComponent, DragonSpawnActionEvent>(OnDragonSpawnAction);
|
||||
|
||||
SubscribeLocalEvent<DragonComponent, DragonStructureDevourComplete>(OnDragonStructureDevourComplete);
|
||||
SubscribeLocalEvent<DragonComponent, DragonDevourCancelledEvent>(OnDragonDevourCancelled);
|
||||
SubscribeLocalEvent<DragonComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(EntityUid uid, DragonComponent component, MobStateChangedEvent args)
|
||||
{
|
||||
//Empties the stomach upon death
|
||||
//TODO: Do this when the dragon gets butchered instead
|
||||
if (args.CurrentMobState.IsDead())
|
||||
{
|
||||
if (component.SoundDeath != null)
|
||||
SoundSystem.Play(component.SoundDeath.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), uid, component.SoundDeath.Params);
|
||||
|
||||
component.DragonStomach.EmptyContainer();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDragonDevourCancelled(EntityUid uid, DragonComponent component, DragonDevourCancelledEvent args)
|
||||
{
|
||||
component.CancelToken = null;
|
||||
}
|
||||
|
||||
private void OnDragonDevourComplete(EntityUid uid, DragonComponent component, DragonDevourComplete args)
|
||||
{
|
||||
component.CancelToken = null;
|
||||
var ichorInjection = new Solution(component.DevourChem, component.DevourHealRate);
|
||||
|
||||
//Humanoid devours allow dragon to get eggs, corpses included
|
||||
if (EntityManager.HasComponent<HumanoidAppearanceComponent>(args.Target))
|
||||
{
|
||||
// Add a spawn for a consumed humanoid
|
||||
component.SpawnsLeft = Math.Min(component.SpawnsLeft + 1, component.MaxSpawns);
|
||||
}
|
||||
//Non-humanoid mobs can only heal dragon for half the normal amount, with no additional spawn tickets
|
||||
else
|
||||
{
|
||||
ichorInjection.ScaleSolution(0.5f);
|
||||
}
|
||||
|
||||
_bloodstreamSystem.TryAddToChemicals(uid, ichorInjection);
|
||||
component.DragonStomach.Insert(args.Target);
|
||||
|
||||
if (component.SoundDevour != null)
|
||||
SoundSystem.Play(component.SoundDevour.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), uid, component.SoundDevour.Params);
|
||||
}
|
||||
|
||||
private void OnDragonStructureDevourComplete(EntityUid uid, DragonComponent component, DragonStructureDevourComplete args)
|
||||
{
|
||||
component.CancelToken = null;
|
||||
//TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow.
|
||||
EntityManager.QueueDeleteEntity(args.Target);
|
||||
|
||||
if (component.SoundDevour != null)
|
||||
SoundSystem.Play(component.SoundDevour.GetSound(), Filter.Pvs(args.User, entityManager: EntityManager), uid, component.SoundDevour.Params);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, DragonComponent component, ComponentStartup args)
|
||||
{
|
||||
component.SpawnsLeft = Math.Min(component.SpawnsLeft, component.MaxSpawns);
|
||||
|
||||
//Dragon doesn't actually chew, since he sends targets right into his stomach.
|
||||
//I did it mom, I added ERP content into upstream. Legally!
|
||||
component.DragonStomach = _containerSystem.EnsureContainer<Container>(uid, "dragon_stomach");
|
||||
|
||||
if (component.DevourAction != null)
|
||||
_actionsSystem.AddAction(uid, component.DevourAction, null);
|
||||
|
||||
if (component.SpawnAction != null)
|
||||
_actionsSystem.AddAction(uid, component.SpawnAction, null);
|
||||
|
||||
if (component.SoundRoar != null)
|
||||
SoundSystem.Play(component.SoundRoar.GetSound(), Filter.Pvs(uid, 4f, EntityManager), uid, component.SoundRoar.Params);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The devour action
|
||||
/// </summary>
|
||||
private void OnDevourAction(EntityUid uid, DragonComponent component, DragonDevourActionEvent args)
|
||||
{
|
||||
if (component.CancelToken != null ||
|
||||
args.Handled ||
|
||||
component.DevourWhitelist?.IsValid(args.Target, EntityManager) != true) return;
|
||||
|
||||
args.Handled = true;
|
||||
var target = args.Target;
|
||||
|
||||
// Structure and mob devours handled differently.
|
||||
if (EntityManager.TryGetComponent(target, out MobStateComponent? targetState))
|
||||
{
|
||||
switch (targetState.CurrentState)
|
||||
{
|
||||
case SharedCriticalMobState:
|
||||
case SharedDeadMobState:
|
||||
component.CancelToken = new CancellationTokenSource();
|
||||
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(uid, component.DevourTime, component.CancelToken.Token, target)
|
||||
{
|
||||
UserFinishedEvent = new DragonDevourComplete(uid, target),
|
||||
UserCancelledEvent = new DragonDevourCancelledEvent(),
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
_popupSystem.PopupEntity(Loc.GetString("devour-action-popup-message-fail-target-alive"), uid, Filter.Entities(uid));
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("devour-action-popup-message-structure"), uid, Filter.Entities(uid));
|
||||
|
||||
if (component.SoundStructureDevour != null)
|
||||
SoundSystem.Play(component.SoundStructureDevour.GetSound(), Filter.Pvs(uid, entityManager: EntityManager), uid, component.SoundStructureDevour.Params);
|
||||
|
||||
component.CancelToken = new CancellationTokenSource();
|
||||
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(uid, component.StructureDevourTime, component.CancelToken.Token, target)
|
||||
{
|
||||
UserFinishedEvent = new DragonStructureDevourComplete(uid, target),
|
||||
UserCancelledEvent = new DragonDevourCancelledEvent(),
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDragonSpawnAction(EntityUid dragonuid, DragonComponent component, DragonSpawnActionEvent args)
|
||||
{
|
||||
if (component.SpawnPrototype == null) return;
|
||||
|
||||
// If dragon has spawns then add one.
|
||||
if (component.SpawnsLeft > 0)
|
||||
{
|
||||
Spawn(component.SpawnPrototype, Transform(dragonuid).Coordinates);
|
||||
component.SpawnsLeft--;
|
||||
return;
|
||||
}
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("dragon-spawn-action-popup-message-fail-no-eggs"), dragonuid, Filter.Entities(dragonuid));
|
||||
}
|
||||
|
||||
private sealed class DragonDevourComplete : EntityEventArgs
|
||||
{
|
||||
public EntityUid User { get; }
|
||||
public EntityUid Target { get; }
|
||||
|
||||
public DragonDevourComplete(EntityUid user, EntityUid target)
|
||||
{
|
||||
User = user;
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DragonStructureDevourComplete : EntityEventArgs
|
||||
{
|
||||
public EntityUid User { get; }
|
||||
public EntityUid Target { get; }
|
||||
|
||||
public DragonStructureDevourComplete(EntityUid user, EntityUid target)
|
||||
{
|
||||
User = user;
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DragonDevourCancelledEvent : EntityEventArgs {}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ namespace Content.Server.Engineering.EntitySystems
|
|||
}
|
||||
private void AddDisassembleVerb(EntityUid uid, DisassembleOnAltVerbComponent component, GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess)
|
||||
if (!args.CanInteract || !args.CanAccess || args.Hands == null)
|
||||
return;
|
||||
|
||||
AlternativeVerb verb = new()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace Content.Server.Entry
|
|||
"InteractionOutline",
|
||||
"MeleeWeaponArcAnimation",
|
||||
"EffectVisuals",
|
||||
"DamageStateVisuals",
|
||||
"AnimationsTest",
|
||||
"ItemStatus",
|
||||
"VehicleVisuals",
|
||||
|
|
@ -24,6 +25,7 @@ namespace Content.Server.Entry
|
|||
"HandheldGPS",
|
||||
"SpentAmmoVisuals",
|
||||
"MagazineVisuals",
|
||||
"PowerCellVisuals",
|
||||
"ToggleableLightVisuals",
|
||||
"CableVisualizer",
|
||||
"PotencyVisuals",
|
||||
|
|
|
|||
|
|
@ -195,12 +195,13 @@ namespace Content.Server.Flash
|
|||
|
||||
private void OnInventoryFlashAttempt(EntityUid uid, InventoryComponent component, FlashAttemptEvent args)
|
||||
{
|
||||
// Forward the event to a worn helmet, if one is equipped.
|
||||
if (_inventorySystem.TryGetSlotEntity(uid, "head", out var maskSlotEntity, component))
|
||||
RaiseLocalEvent(maskSlotEntity.Value, args);
|
||||
// Forward the event to the glasses, if any.
|
||||
if(!args.Cancelled && _inventorySystem.TryGetSlotEntity(uid, "eyes", out var eyeSlotEntity, component))
|
||||
RaiseLocalEvent(eyeSlotEntity.Value, args);
|
||||
foreach (var slot in new string[]{"head", "eyes", "mask"})
|
||||
{
|
||||
if (args.Cancelled)
|
||||
break;
|
||||
if (_inventorySystem.TryGetSlotEntity(uid, slot, out var item, component))
|
||||
RaiseLocalEvent(item.Value, args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFlashImmunityFlashAttempt(EntityUid uid, FlashImmunityComponent component, FlashAttemptEvent args)
|
||||
|
|
|
|||
|
|
@ -6,28 +6,14 @@ namespace Content.Server.Holosign
|
|||
[RegisterComponent]
|
||||
public sealed class HolosignProjectorComponent : Component
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("maxCharges")]
|
||||
public int MaxCharges = 6;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("charges")]
|
||||
public int CurrentCharges = 6;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("signProto", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string SignProto = "HolosignWetFloor";
|
||||
|
||||
/// <summary>
|
||||
/// When the holosign was last used.
|
||||
/// How much charge a single use expends.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("lastUse")]
|
||||
public TimeSpan LastUsed = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes for 1 charge to accumulate.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("rechargeTime")]
|
||||
public TimeSpan RechargeTime = TimeSpan.FromSeconds(30);
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("chargeUse")]
|
||||
public float ChargeUse = 50f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Server.Coordinates.Helpers;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Holosign
|
||||
{
|
||||
public sealed class HolosignSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PowerCellSystem _cellSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -16,42 +19,51 @@ namespace Content.Server.Holosign
|
|||
SubscribeLocalEvent<HolosignProjectorComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private int GetCharges(HolosignProjectorComponent component)
|
||||
{
|
||||
return component.CurrentCharges + (int) ((_timing.CurTime - component.LastUsed).TotalSeconds / component.RechargeTime.TotalSeconds);
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, HolosignProjectorComponent component, ExaminedEvent args)
|
||||
{
|
||||
// TODO: This should probably be using an itemstatus
|
||||
// TODO: I'm too lazy to do this rn but it's literally copy-paste from emag.
|
||||
var timeRemaining = (component.LastUsed + component.RechargeTime * (component.MaxCharges - component.CurrentCharges) - _timing.CurTime).TotalSeconds % component.RechargeTime.TotalSeconds;
|
||||
var charges = GetCharges(component);
|
||||
_cellSystem.TryGetBatteryFromSlot(uid, out var battery);
|
||||
var charges = UsesRemaining(component, battery);
|
||||
var maxCharges = MaxUses(component, battery);
|
||||
|
||||
args.PushMarkup(Loc.GetString("emag-charges-remaining", ("charges", charges)));
|
||||
if (charges == component.MaxCharges)
|
||||
|
||||
if (charges > 0 && charges == maxCharges)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("emag-max-charges"));
|
||||
return;
|
||||
}
|
||||
args.PushMarkup(Loc.GetString("emag-recharging", ("seconds", Math.Round(timeRemaining))));
|
||||
}
|
||||
|
||||
private void OnUse(EntityUid uid, HolosignProjectorComponent component, UseInHandEvent args)
|
||||
{
|
||||
if (component.CurrentCharges == 0 || args.Handled)
|
||||
if (args.Handled ||
|
||||
!_cellSystem.TryGetBatteryFromSlot(uid, out var battery) ||
|
||||
!battery.TryUseCharge(component.ChargeUse))
|
||||
return;
|
||||
|
||||
// TODO: Too tired to deal
|
||||
var holo = EntityManager.SpawnEntity(component.SignProto, Transform(args.User).Coordinates.SnapToGrid(EntityManager));
|
||||
Transform(holo).Anchored = true;
|
||||
|
||||
// Don't reset last use time if it's already accumulating.
|
||||
if (component.CurrentCharges == component.MaxCharges)
|
||||
component.LastUsed = _timing.CurTime;
|
||||
|
||||
component.CurrentCharges--;
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private int UsesRemaining(HolosignProjectorComponent component, BatteryComponent? battery = null)
|
||||
{
|
||||
if (battery == null ||
|
||||
component.ChargeUse == 0f) return 0;
|
||||
|
||||
return (int) (battery.CurrentCharge / component.ChargeUse);
|
||||
}
|
||||
|
||||
private int MaxUses(HolosignProjectorComponent component, BatteryComponent? battery = null)
|
||||
{
|
||||
if (battery == null ||
|
||||
component.ChargeUse == 0f) return 0;
|
||||
|
||||
return (int) (battery.MaxCharge / component.ChargeUse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,6 +197,12 @@ namespace Content.Server.Interaction
|
|||
if (!unobstructed)
|
||||
return;
|
||||
}
|
||||
else if (ContainerSystem.IsEntityInContainer(user))
|
||||
{
|
||||
// No wide attacking while in containers (holos, lockers, etc).
|
||||
// Can't think of a valid case where you would want this.
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify user has a hand, and find what object they are currently holding in their active hand
|
||||
if (TryComp(user, out HandsComponent? hands))
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ public sealed class SharpSystem : EntitySystem
|
|||
|
||||
private void OnGetInteractionVerbs(EntityUid uid, SharedButcherableComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (component.Type != ButcheringType.Knife)
|
||||
if (component.Type != ButcheringType.Knife || args.Hands == null)
|
||||
return;
|
||||
|
||||
bool disabled = false;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Content.Server.Actions;
|
||||
using Content.Server.Light.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
|
|
@ -220,7 +221,8 @@ namespace Content.Server.Light.EntitySystems
|
|||
{
|
||||
if (component.Activated) return false;
|
||||
|
||||
if (!_powerCell.TryGetBatteryFromSlot(component.Owner, out var battery))
|
||||
if (!_powerCell.TryGetBatteryFromSlot(component.Owner, out var battery) &&
|
||||
!TryComp(component.Owner, out battery))
|
||||
{
|
||||
SoundSystem.Play(component.TurnOnFailSound.GetSound(), Filter.Pvs(component.Owner, entityManager: EntityManager), component.Owner);
|
||||
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), component.Owner, Filter.Entities(user));
|
||||
|
|
@ -253,7 +255,8 @@ namespace Content.Server.Light.EntitySystems
|
|||
|
||||
public void TryUpdate(HandheldLightComponent component, float frameTime)
|
||||
{
|
||||
if (!_powerCell.TryGetBatteryFromSlot(component.Owner, out var battery))
|
||||
if (!_powerCell.TryGetBatteryFromSlot(component.Owner, out var battery) &&
|
||||
!TryComp(component.Owner, out battery))
|
||||
{
|
||||
TurnOff(component, false);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ namespace Content.Server.Medical.SuitSensors
|
|||
return;
|
||||
|
||||
// standard interaction checks
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
|
||||
return;
|
||||
|
||||
args.Verbs.UnionWith(new[]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.EntitySystems;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Vehicle.Components;
|
||||
using Content.Shared.Movement;
|
||||
using Content.Shared.Movement.Components;
|
||||
|
|
@ -123,7 +123,7 @@ namespace Content.Server.Physics.Controllers
|
|||
angularInput += sprint.X;
|
||||
}
|
||||
break;
|
||||
case ShuttleMode.Docking:
|
||||
case ShuttleMode.Strafing:
|
||||
// No angular input possible
|
||||
foreach (var (pilot, mover) in pilots)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ namespace Content.Server.Polymorph.Systems
|
|||
/// </summary>
|
||||
/// <param name="target">The entity that will be transformed</param>
|
||||
/// <param name="id">The id of the polymorph prototype</param>
|
||||
public EntityUid? PolymorphEntity(EntityUid target, String id)
|
||||
public EntityUid? PolymorphEntity(EntityUid target, string id)
|
||||
{
|
||||
if (!_proto.TryIndex<PolymorphPrototype>(id, out var proto))
|
||||
{
|
||||
|
|
@ -99,9 +99,8 @@ namespace Content.Server.Polymorph.Systems
|
|||
comp.Prototype = proto;
|
||||
RaiseLocalEvent(child, new PolymorphComponentSetupEvent());
|
||||
|
||||
var targetXform = Transform(target);
|
||||
var childXform = Transform(child);
|
||||
childXform.LocalRotation = targetXform.LocalRotation;
|
||||
childXform.LocalRotation = targetTransformComp.LocalRotation;
|
||||
|
||||
if (_container.TryGetContainingContainer(target, out var cont))
|
||||
cont.Insert(child);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ namespace Content.Server.Polymorph.Systems
|
|||
if (!TryComp<PolymorphedEntityComponent>(uid, out var component))
|
||||
return;
|
||||
|
||||
if (Deleted(component.Parent))
|
||||
return;
|
||||
|
||||
var proto = component.Prototype;
|
||||
|
||||
var uidXform = Transform(uid);
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
namespace Content.Server.Radar;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class RadarConsoleComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("range")]
|
||||
public float Range = 512f;
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Radar;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Radar;
|
||||
|
||||
public sealed class RadarConsoleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private static float Frequency = 1.5f;
|
||||
|
||||
private float _accumulator;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
_accumulator += frameTime;
|
||||
|
||||
if (_accumulator < Frequency)
|
||||
return;
|
||||
|
||||
_accumulator -= Frequency;
|
||||
|
||||
foreach (var (component, xform) in EntityManager.EntityQuery<RadarConsoleComponent, TransformComponent>())
|
||||
{
|
||||
var s = component.Owner.GetUIOrNull(RadarConsoleUiKey.Key);
|
||||
|
||||
if (s is null || s.SubscribedSessions.Count == 0)
|
||||
continue;
|
||||
|
||||
var (radarPos, _, radarInvMatrix) = xform.GetWorldPositionRotationInvMatrix();
|
||||
|
||||
var mapId = xform.MapID;
|
||||
var objects = new List<RadarObjectData>();
|
||||
|
||||
_mapManager.FindGridsIntersectingEnumerator(mapId, new Box2(radarPos - component.Range, radarPos + component.Range), out var enumerator, true);
|
||||
|
||||
while (enumerator.MoveNext(out var grid))
|
||||
{
|
||||
var phy = Comp<PhysicsComponent>(grid.GridEntityId);
|
||||
var transform = Transform(grid.GridEntityId);
|
||||
|
||||
if (phy.Mass < 50)
|
||||
continue;
|
||||
|
||||
var rad = Math.Log2(phy.Mass);
|
||||
var gridCenter = transform.WorldMatrix.Transform(phy.LocalCenter);
|
||||
|
||||
var pos = radarInvMatrix.Transform(gridCenter);
|
||||
pos.Y = -pos.Y; // Robust has an inverted Y, like BYOND. This undoes that.
|
||||
|
||||
if (pos.Length > component.Range)
|
||||
continue;
|
||||
|
||||
objects.Add(new RadarObjectData {Color = Color.Aqua, Position = pos, Radius = (float)rad});
|
||||
}
|
||||
|
||||
s.SetState(new RadarConsoleBoundInterfaceState(component.Range, objects.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Content.Server/RatKing/RatKingComponent.cs
Normal file
54
Content.Server/RatKing/RatKingComponent.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.RatKing
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class RatKingComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The action for the Raise Army ability
|
||||
/// </summary>
|
||||
[DataField("actionRaiseArmy", required: true)]
|
||||
public InstantAction ActionRaiseArmy = new();
|
||||
|
||||
/// <summary>
|
||||
/// The amount of hunger one use of Raise Army consumes
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("hungerPerArmyUse", required: true)]
|
||||
public float HungerPerArmyUse = 25f;
|
||||
|
||||
/// <summary>
|
||||
/// The entity prototype of the mob that Raise Army summons
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("armyMobSpawnId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string ArmyMobSpawnId = "MobRatServant";
|
||||
|
||||
/// <summary>
|
||||
/// The action for the Domain ability
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("actionDomain", required: true)]
|
||||
public InstantAction ActionDomain = new();
|
||||
|
||||
/// <summary>
|
||||
/// The amount of hunger one use of Domain consumes
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("hungerPerDomainUse", required: true)]
|
||||
public float HungerPerDomainUse = 50f;
|
||||
|
||||
/// <summary>
|
||||
/// The disease prototype id that the Domain ability spreads
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("domainDiseaseId", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
public string DomainDiseaseId = "Plague";
|
||||
|
||||
/// <summary>
|
||||
/// The range of the Domain ability.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("domainRange")]
|
||||
public float DomainRange = 3f;
|
||||
|
||||
}
|
||||
};
|
||||
93
Content.Server/RatKing/RatKingSystem.cs
Normal file
93
Content.Server/RatKing/RatKingSystem.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using Content.Server.Actions;
|
||||
using Content.Server.Disease;
|
||||
using Content.Server.Disease.Components;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.RatKing
|
||||
{
|
||||
public sealed class RatKingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly ActionsSystem _action = default!;
|
||||
[Dependency] private readonly DiseaseSystem _disease = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RatKingComponent, ComponentStartup>(OnStartup);
|
||||
|
||||
SubscribeLocalEvent<RatKingComponent, RatKingRaiseArmyActionEvent>(OnRaiseArmy);
|
||||
SubscribeLocalEvent<RatKingComponent, RatKingDomainActionEvent>(OnDomain);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, RatKingComponent component, ComponentStartup args)
|
||||
{
|
||||
_action.AddAction(uid, component.ActionRaiseArmy, null);
|
||||
_action.AddAction(uid, component.ActionDomain, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summons an allied rat servant at the King, costing a small amount of hunger
|
||||
/// </summary>
|
||||
private void OnRaiseArmy(EntityUid uid, RatKingComponent component, RatKingRaiseArmyActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryComp<HungerComponent>(uid, out var hunger))
|
||||
return;
|
||||
|
||||
//make sure the hunger doesn't go into the negatives
|
||||
if (hunger.CurrentHunger < component.HungerPerArmyUse)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, Filter.Entities(uid));
|
||||
return;
|
||||
}
|
||||
args.Handled = true;
|
||||
hunger.CurrentHunger -= component.HungerPerArmyUse;
|
||||
Spawn(component.ArmyMobSpawnId, Transform(uid).Coordinates); //spawn the little mouse boi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all of the nearby disease-carrying entities in a radius
|
||||
/// and gives them the specified disease. It has a hunger cost as well
|
||||
/// </summary>
|
||||
private void OnDomain(EntityUid uid, RatKingComponent component, RatKingDomainActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryComp<HungerComponent>(uid, out var hunger))
|
||||
return;
|
||||
|
||||
//make sure the hunger doesn't go into the negatives
|
||||
if (hunger.CurrentHunger < component.HungerPerDomainUse)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, Filter.Entities(uid));
|
||||
return;
|
||||
}
|
||||
args.Handled = true;
|
||||
hunger.CurrentHunger -= component.HungerPerDomainUse;
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("rat-king-domain-popup"), uid, Filter.Pvs(uid, default, EntityManager));
|
||||
|
||||
var tstalker = GetEntityQuery<DiseaseCarrierComponent>();
|
||||
foreach (var entity in _lookup.GetEntitiesInRange(uid, component.DomainRange)) //go through all of them, filtering only those in range that are not the king itself
|
||||
{
|
||||
if (entity == uid)
|
||||
continue;
|
||||
|
||||
if (tstalker.TryGetComponent(entity, out var diseasecomp))
|
||||
_disease.TryInfect(diseasecomp, component.DomainDiseaseId); //infect them with w/e disease
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RatKingRaiseArmyActionEvent : InstantActionEvent { };
|
||||
public sealed class RatKingDomainActionEvent : InstantActionEvent { };
|
||||
};
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Doors.Components;
|
||||
|
|
@ -8,7 +7,9 @@ using Content.Shared.Physics;
|
|||
using Content.Shared.Access.Components;
|
||||
using Content.Server.Doors.Systems;
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using static Content.Server.Remotes.DoorRemoteComponent;
|
||||
|
||||
namespace Content.Server.Remotes
|
||||
{
|
||||
|
|
@ -21,80 +22,81 @@ namespace Content.Server.Remotes
|
|||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DoorRemoteComponent, UseInHandEvent>(OnInHandActivation);
|
||||
SubscribeLocalEvent<DoorRemoteComponent, BeforeRangedInteractEvent>(OnBeforeInteract);
|
||||
}
|
||||
|
||||
public void OnInHandActivation(EntityUid user, DoorRemoteComponent component, UseInHandEvent args)
|
||||
{
|
||||
string switchMessageId;
|
||||
switch (component.Mode)
|
||||
{
|
||||
case DoorRemoteComponent.OperatingMode.OpenClose:
|
||||
component.Mode = DoorRemoteComponent.OperatingMode.ToggleBolts;
|
||||
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-toggle-bolts"), args.User, Filter.Entities(args.User));
|
||||
case OperatingMode.OpenClose:
|
||||
component.Mode = OperatingMode.ToggleBolts;
|
||||
switchMessageId = "door-remote-switch-state-toggle-bolts";
|
||||
break;
|
||||
case DoorRemoteComponent.OperatingMode.ToggleBolts:
|
||||
component.Mode = DoorRemoteComponent.OperatingMode.ToggleEmergencyAccess;
|
||||
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-toggle-emergency-access"), args.User, Filter.Entities(args.User));
|
||||
case OperatingMode.ToggleBolts:
|
||||
component.Mode = OperatingMode.ToggleEmergencyAccess;
|
||||
switchMessageId = "door-remote-switch-state-toggle-emergency-access";
|
||||
break;
|
||||
case DoorRemoteComponent.OperatingMode.ToggleEmergencyAccess:
|
||||
component.Mode = DoorRemoteComponent.OperatingMode.OpenClose;
|
||||
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-open-close"), args.User, Filter.Entities(args.User));
|
||||
case OperatingMode.ToggleEmergencyAccess:
|
||||
component.Mode = OperatingMode.OpenClose;
|
||||
switchMessageId = "door-remote-switch-state-open-close";
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(DoorRemoteComponent)} had invalid mode {component.Mode}");
|
||||
}
|
||||
ShowPopupToUser(switchMessageId, args.User);
|
||||
}
|
||||
|
||||
private void OnBeforeInteract(EntityUid uid, DoorRemoteComponent component, BeforeRangedInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach ||
|
||||
args.Handled
|
||||
if (args.Handled
|
||||
|| args.Target == null
|
||||
|| !TryComp<DoorComponent>(args.Target, out var doorComponent) // If it isn't a door we don't use it
|
||||
|| !HasComp<AccessReaderComponent>(args.Target) // Remotes do not work on doors without access requirements
|
||||
|| !TryComp<AirlockComponent>(args.Target, out var airlockComponent) // Remotes only work on airlocks
|
||||
// TODO: Why the fuck is this -1f
|
||||
|| !_interactionSystem.InRangeUnobstructed(args.User, doorComponent.Owner, -1f, CollisionGroup.Opaque))
|
||||
{
|
||||
|| !TryComp<DoorComponent>(args.Target, out var doorComp) // If it isn't a door we don't use it
|
||||
|| !TryComp<AirlockComponent>(args.Target, out var airlockComp) // Remotes only work on airlocks
|
||||
// The remote can be used anywhere the user can see the door.
|
||||
// This doesn't work that well, but I don't know of an alternative
|
||||
|| !_interactionSystem.InRangeUnobstructed(args.User, args.Target.Value,
|
||||
SharedInteractionSystem.MaxRaycastRange, CollisionGroup.Opaque))
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (component.Mode == DoorRemoteComponent.OperatingMode.OpenClose)
|
||||
if (!this.IsPowered(args.Target.Value, EntityManager))
|
||||
{
|
||||
_doorSystem.TryToggleDoor(doorComponent.Owner, user: args.Used);
|
||||
ShowPopupToUser("door-remote-no-power", args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.Mode == DoorRemoteComponent.OperatingMode.ToggleBolts
|
||||
&& airlockComponent.IsPowered())
|
||||
if (TryComp<AccessReaderComponent>(args.Target, out var accessComponent) &&
|
||||
!_doorSystem.HasAccess(doorComp.Owner, args.Used, accessComponent))
|
||||
{
|
||||
if (_doorSystem.HasAccess(doorComponent.Owner, args.Used))
|
||||
{
|
||||
airlockComponent.SetBoltsWithAudio(!airlockComponent.IsBolted());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (doorComponent.State != DoorState.Open)
|
||||
{
|
||||
_doorSystem.Deny(airlockComponent.Owner, user: args.User);
|
||||
}
|
||||
else if (doorComponent.DenySound != null)
|
||||
{
|
||||
SoundSystem.Play(doorComponent.DenySound.GetSound(), Filter.Pvs(args.Target.Value, entityManager: EntityManager), args.Target.Value);
|
||||
}
|
||||
}
|
||||
_doorSystem.Deny(airlockComp.Owner, doorComp, args.User);
|
||||
ShowPopupToUser("door-remote-denied", args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.Mode == DoorRemoteComponent.OperatingMode.ToggleEmergencyAccess
|
||||
&& airlockComponent.IsPowered())
|
||||
switch (component.Mode)
|
||||
{
|
||||
if (_doorSystem.HasAccess(doorComponent.Owner, args.Used))
|
||||
{
|
||||
_sharedAirlockSystem.ToggleEmergencyAccess(airlockComponent);
|
||||
}
|
||||
case OperatingMode.OpenClose:
|
||||
_doorSystem.TryToggleDoor(doorComp.Owner, doorComp, args.Used);
|
||||
break;
|
||||
case OperatingMode.ToggleBolts:
|
||||
//TODO: What about cut wires...?
|
||||
airlockComp.SetBoltsWithAudio(!airlockComp.IsBolted());
|
||||
break;
|
||||
case OperatingMode.ToggleEmergencyAccess:
|
||||
_sharedAirlockSystem.ToggleEmergencyAccess(airlockComp);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(DoorRemoteComponent)} had invalid mode {component.Mode}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPopupToUser(string messageId, EntityUid user) =>
|
||||
_popupSystem.PopupEntity(Loc.GetString(messageId), user, Filter.Entities(user));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
Content.Server/Shuttles/Components/AutoDockComponent.cs
Normal file
14
Content.Server/Shuttles/Components/AutoDockComponent.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace Content.Server.Shuttles.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to entities when they are actively trying to dock with something else.
|
||||
/// We track it because checking every dock constantly would be expensive.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AutoDockComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Track who has requested autodocking so we can know when to be removed.
|
||||
/// </summary>
|
||||
public HashSet<EntityUid> Requesters = new();
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace Content.Server.Shuttles.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to <see cref="DockingComponent"/> that have recently undocked.
|
||||
/// This checks for whether they've left the specified radius before allowing them to automatically dock again.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class RecentlyDockedComponent : Component
|
||||
{
|
||||
[ViewVariables, DataField("lastDocked")]
|
||||
public EntityUid LastDocked;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("radius")]
|
||||
public float Radius = 1.5f;
|
||||
}
|
||||
|
|
@ -3,8 +3,14 @@ using Content.Shared.Shuttles.Components;
|
|||
namespace Content.Server.Shuttles.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ShuttleComponent : SharedShuttleComponent
|
||||
public sealed class ShuttleComponent : Component
|
||||
{
|
||||
[ViewVariables]
|
||||
public bool Enabled = true;
|
||||
|
||||
[ViewVariables]
|
||||
public ShuttleMode Mode = ShuttleMode.Cruise;
|
||||
|
||||
/// <summary>
|
||||
/// The cached thrust available for each cardinal direction
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,10 @@ using Content.Shared.Shuttles.Components;
|
|||
namespace Content.Server.Shuttles.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedShuttleConsoleComponent))]
|
||||
internal sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent
|
||||
public sealed class ShuttleConsoleComponent : SharedShuttleConsoleComponent
|
||||
{
|
||||
[ViewVariables]
|
||||
public List<PilotComponent> SubscribedPilots = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the console can be used to pilot. Toggled whenever it gets powered / unpowered.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool Enabled { get; set; } = false;
|
||||
public readonly List<PilotComponent> SubscribedPilots = new();
|
||||
|
||||
/// <summary>
|
||||
/// How much should the pilot's eye be zoomed by when piloting using this console?
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Server.Shuttles.EntitySystems;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Damage;
|
||||
|
||||
namespace Content.Server.Shuttles.Components
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Content.Server.Administration;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.EntitySystems;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,250 +0,0 @@
|
|||
using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Shuttles;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Shuttles.EntitySystems
|
||||
{
|
||||
internal sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly TagSystem _tags = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, ComponentShutdown>(HandleConsoleShutdown);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, ActivateInWorldEvent>(HandleConsoleInteract);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, PowerChangedEvent>(HandlePowerChange);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, GetVerbsEvent<InteractionVerb>>(OnConsoleInteract);
|
||||
|
||||
SubscribeLocalEvent<PilotComponent, MoveEvent>(HandlePilotMove);
|
||||
}
|
||||
|
||||
private void OnConsoleInteract(EntityUid uid, ShuttleConsoleComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (!args.CanAccess ||
|
||||
!args.CanInteract)
|
||||
return;
|
||||
|
||||
var xform = EntityManager.GetComponent<TransformComponent>(uid);
|
||||
|
||||
// Maybe move mode onto the console instead?
|
||||
if (!_mapManager.TryGetGrid(xform.GridEntityId, out var grid) ||
|
||||
!EntityManager.TryGetComponent(grid.GridEntityId, out ShuttleComponent? shuttle)) return;
|
||||
|
||||
InteractionVerb verb = new()
|
||||
{
|
||||
Text = Loc.GetString("shuttle-mode-toggle"),
|
||||
Act = () => ToggleShuttleMode(args.User, component, shuttle),
|
||||
Disabled = !xform.Anchored || !this.IsPowered(uid, EntityManager),
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void ToggleShuttleMode(EntityUid user, ShuttleConsoleComponent consoleComponent, ShuttleComponent shuttleComponent, TransformComponent? consoleXform = null)
|
||||
{
|
||||
// Re-validate
|
||||
if (!this.IsPowered(consoleComponent.Owner, EntityManager)) return;
|
||||
|
||||
if (!Resolve(consoleComponent.Owner, ref consoleXform)) return;
|
||||
|
||||
if (!consoleXform.Anchored || consoleXform.GridEntityId != EntityManager.GetComponent<TransformComponent>(shuttleComponent.Owner).GridEntityId) return;
|
||||
|
||||
switch (shuttleComponent.Mode)
|
||||
{
|
||||
case ShuttleMode.Cruise:
|
||||
shuttleComponent.Mode = ShuttleMode.Docking;
|
||||
_popup.PopupEntity(Loc.GetString("shuttle-mode-docking"), consoleComponent.Owner, Filter.Entities(user));
|
||||
break;
|
||||
case ShuttleMode.Docking:
|
||||
shuttleComponent.Mode = ShuttleMode.Cruise;
|
||||
_popup.PopupEntity(Loc.GetString("shuttle-mode-cruise"), consoleComponent.Owner, Filter.Entities(user));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var toRemove = new RemQueue<PilotComponent>();
|
||||
|
||||
foreach (var comp in EntityManager.EntityQuery<PilotComponent>())
|
||||
{
|
||||
if (comp.Console == null) continue;
|
||||
|
||||
if (!_blocker.CanInteract(comp.Owner, comp.Console.Owner))
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var comp in toRemove)
|
||||
{
|
||||
RemovePilot(comp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Console requires power to operate.
|
||||
/// </summary>
|
||||
private void HandlePowerChange(EntityUid uid, ShuttleConsoleComponent component, PowerChangedEvent args)
|
||||
{
|
||||
if (!args.Powered)
|
||||
{
|
||||
component.Enabled = false;
|
||||
|
||||
ClearPilots(component);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If pilot is moved then we'll stop them from piloting.
|
||||
/// </summary>
|
||||
private void HandlePilotMove(EntityUid uid, PilotComponent component, ref MoveEvent args)
|
||||
{
|
||||
if (component.Console == null || component.Position == null)
|
||||
{
|
||||
DebugTools.Assert(component.Position == null && component.Console == null);
|
||||
EntityManager.RemoveComponent<PilotComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.NewPosition.TryDistance(EntityManager, component.Position.Value, out var distance) &&
|
||||
distance < PilotComponent.BreakDistance) return;
|
||||
|
||||
RemovePilot(component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For now pilots just interact with the console and can start piloting with wasd.
|
||||
/// </summary>
|
||||
private void HandleConsoleInteract(EntityUid uid, ShuttleConsoleComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (!_tags.HasTag(args.User, "CanPilot"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pilotComponent = EntityManager.EnsureComponent<PilotComponent>(args.User);
|
||||
|
||||
if (!component.Enabled)
|
||||
{
|
||||
args.User.PopupMessage($"Console is not powered.");
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
var console = pilotComponent.Console;
|
||||
|
||||
if (console != null)
|
||||
{
|
||||
RemovePilot(pilotComponent);
|
||||
|
||||
if (console == component)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AddPilot(args.User, component);
|
||||
}
|
||||
|
||||
protected override void HandlePilotShutdown(EntityUid uid, PilotComponent component, ComponentShutdown args)
|
||||
{
|
||||
base.HandlePilotShutdown(uid, component, args);
|
||||
RemovePilot(component);
|
||||
}
|
||||
|
||||
private void HandleConsoleShutdown(EntityUid uid, ShuttleConsoleComponent component, ComponentShutdown args)
|
||||
{
|
||||
ClearPilots(component);
|
||||
}
|
||||
|
||||
public void AddPilot(EntityUid entity, ShuttleConsoleComponent component)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent) ||
|
||||
component.SubscribedPilots.Contains(pilotComponent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<SharedEyeComponent>(entity, out var eye))
|
||||
{
|
||||
eye.Zoom = component.Zoom;
|
||||
}
|
||||
|
||||
component.SubscribedPilots.Add(pilotComponent);
|
||||
|
||||
_alertsSystem.ShowAlert(entity, AlertType.PilotingShuttle);
|
||||
|
||||
entity.PopupMessage(Loc.GetString("shuttle-pilot-start"));
|
||||
pilotComponent.Console = component;
|
||||
ActionBlockerSystem.UpdateCanMove(entity);
|
||||
pilotComponent.Position = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
|
||||
pilotComponent.Dirty();
|
||||
}
|
||||
|
||||
public void RemovePilot(PilotComponent pilotComponent)
|
||||
{
|
||||
var console = pilotComponent.Console;
|
||||
|
||||
if (console is not ShuttleConsoleComponent helmsman) return;
|
||||
|
||||
pilotComponent.Console = null;
|
||||
pilotComponent.Position = null;
|
||||
|
||||
if (TryComp<SharedEyeComponent>(pilotComponent.Owner, out var eye))
|
||||
{
|
||||
eye.Zoom = new(1.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (!helmsman.SubscribedPilots.Remove(pilotComponent)) return;
|
||||
|
||||
_alertsSystem.ClearAlert(pilotComponent.Owner, AlertType.PilotingShuttle);
|
||||
|
||||
pilotComponent.Owner.PopupMessage(Loc.GetString("shuttle-pilot-end"));
|
||||
|
||||
if (pilotComponent.LifeStage < ComponentLifeStage.Stopping)
|
||||
EntityManager.RemoveComponent<PilotComponent>(pilotComponent.Owner);
|
||||
}
|
||||
|
||||
public void RemovePilot(EntityUid entity)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent)) return;
|
||||
|
||||
RemovePilot(pilotComponent);
|
||||
}
|
||||
|
||||
public void ClearPilots(ShuttleConsoleComponent component)
|
||||
{
|
||||
while (component.SubscribedPilots.TryGetValue(0, out var pilot))
|
||||
{
|
||||
RemovePilot(pilot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Content.Server/Shuttles/Events/DockEvent.cs
Normal file
15
Content.Server/Shuttles/Events/DockEvent.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using Content.Server.Shuttles.Components;
|
||||
|
||||
namespace Content.Server.Shuttles.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever 2 airlocks dock.
|
||||
/// </summary>
|
||||
public sealed class DockEvent : EntityEventArgs
|
||||
{
|
||||
public DockingComponent DockA = default!;
|
||||
public DockingComponent DockB = default!;
|
||||
|
||||
public EntityUid GridAUid = default!;
|
||||
public EntityUid GridBUid = default!;
|
||||
}
|
||||
15
Content.Server/Shuttles/Events/UndockEvent.cs
Normal file
15
Content.Server/Shuttles/Events/UndockEvent.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using Content.Server.Shuttles.Components;
|
||||
|
||||
namespace Content.Server.Shuttles.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever 2 grids undock.
|
||||
/// </summary>
|
||||
public sealed class UndockEvent : EntityEventArgs
|
||||
{
|
||||
public DockingComponent DockA = default!;
|
||||
public DockingComponent DockB = default!;
|
||||
|
||||
public EntityUid GridAUid = default!;
|
||||
public EntityUid GridBUid = default!;
|
||||
}
|
||||
104
Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs
Normal file
104
Content.Server/Shuttles/Systems/DockingSystem.AutoDock.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using Content.Server.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Shuttles.Systems;
|
||||
|
||||
public sealed partial class DockingSystem
|
||||
{
|
||||
private void UpdateAutodock()
|
||||
{
|
||||
// Work out what we can autodock with, what we shouldn't, and when we should stop tracking.
|
||||
// Autodocking only stops when the client closes that dock viewport OR they lose pilotcomponent.
|
||||
var dockingQuery = GetEntityQuery<DockingComponent>();
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var recentQuery = GetEntityQuery<RecentlyDockedComponent>();
|
||||
|
||||
foreach (var (comp, body) in EntityQuery<AutoDockComponent, PhysicsComponent>())
|
||||
{
|
||||
if (comp.Requesters.Count == 0 || !dockingQuery.TryGetComponent(comp.Owner, out var dock))
|
||||
{
|
||||
RemComp<AutoDockComponent>(comp.Owner);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't re-dock if we're already docked or recently were.
|
||||
if (dock.Docked || recentQuery.HasComponent(comp.Owner)) continue;
|
||||
|
||||
var dockable = GetDockable(body, xformQuery.GetComponent(comp.Owner));
|
||||
|
||||
if (dockable == null) continue;
|
||||
|
||||
TryDock(dock, dockable);
|
||||
}
|
||||
|
||||
// Work out recent docks that have gone past their designated threshold.
|
||||
var checkedRecent = new HashSet<EntityUid>();
|
||||
|
||||
foreach (var (comp, xform) in EntityQuery<RecentlyDockedComponent, TransformComponent>())
|
||||
{
|
||||
if (!checkedRecent.Add(comp.Owner)) continue;
|
||||
|
||||
if (!dockingQuery.TryGetComponent(comp.Owner, out var dock))
|
||||
{
|
||||
RemComp<RecentlyDockedComponent>(comp.Owner);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!xformQuery.TryGetComponent(comp.LastDocked, out var otherXform))
|
||||
{
|
||||
RemComp<RecentlyDockedComponent>(comp.Owner);
|
||||
continue;
|
||||
}
|
||||
|
||||
var worldPos = _transformSystem.GetWorldPosition(xform, xformQuery);
|
||||
var otherWorldPos = _transformSystem.GetWorldPosition(otherXform, xformQuery);
|
||||
|
||||
if ((worldPos - otherWorldPos).Length < comp.Radius) continue;
|
||||
|
||||
_sawmill.Debug($"Removed RecentlyDocked from {ToPrettyString(comp.Owner)} and {ToPrettyString(comp.LastDocked)}");
|
||||
RemComp<RecentlyDockedComponent>(comp.Owner);
|
||||
RemComp<RecentlyDockedComponent>(comp.LastDocked);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRequestUndock(EntityUid uid, ShuttleConsoleComponent component, UndockRequestMessage args)
|
||||
{
|
||||
_sawmill.Debug($"Received undock request for {ToPrettyString(args.Entity)}");
|
||||
|
||||
// TODO: Validation
|
||||
if (!TryComp<DockingComponent>(args.Entity, out var dock) ||
|
||||
!dock.Docked) return;
|
||||
|
||||
Undock(dock);
|
||||
}
|
||||
|
||||
private void OnRequestAutodock(EntityUid uid, ShuttleConsoleComponent component, AutodockRequestMessage args)
|
||||
{
|
||||
_sawmill.Debug($"Received autodock request for {ToPrettyString(args.Entity)}");
|
||||
var player = args.Session.AttachedEntity;
|
||||
|
||||
if (player == null || !HasComp<DockingComponent>(args.Entity)) return;
|
||||
|
||||
// TODO: Validation
|
||||
var comp = EnsureComp<AutoDockComponent>(args.Entity);
|
||||
comp.Requesters.Add(player.Value);
|
||||
}
|
||||
|
||||
private void OnRequestStopAutodock(EntityUid uid, ShuttleConsoleComponent component, StopAutodockRequestMessage args)
|
||||
{
|
||||
_sawmill.Debug($"Received stop autodock request for {ToPrettyString(args.Entity)}");
|
||||
|
||||
var player = args.Session.AttachedEntity;
|
||||
|
||||
// TODO: Validation
|
||||
if (player == null || !TryComp<AutoDockComponent>(args.Entity, out var comp)) return;
|
||||
|
||||
comp.Requesters.Remove(player.Value);
|
||||
|
||||
if (comp.Requesters.Count == 0)
|
||||
RemComp<AutoDockComponent>(args.Entity);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,30 @@
|
|||
using Content.Server.Doors.Systems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.Events;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Shuttles.EntitySystems
|
||||
namespace Content.Server.Shuttles.Systems
|
||||
{
|
||||
public sealed class DockingSystem : EntitySystem
|
||||
public sealed partial class DockingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly FixtureSystem _fixtureSystem = default!;
|
||||
[Dependency] private readonly SharedJointSystem _jointSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly ShuttleConsoleSystem _console = default!;
|
||||
[Dependency] private readonly DoorSystem _doorSystem = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private const string DockingFixture = "docking";
|
||||
private const string DockingJoint = "docking";
|
||||
private const float DockingRadius = 0.20f;
|
||||
|
|
@ -26,14 +32,26 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_sawmill = Logger.GetSawmill("docking");
|
||||
SubscribeLocalEvent<DockingComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<DockingComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<DockingComponent, PowerChangedEvent>(OnPowerChange);
|
||||
SubscribeLocalEvent<DockingComponent, AnchorStateChangedEvent>(OnAnchorChange);
|
||||
SubscribeLocalEvent<DockingComponent, ReAnchorEvent>(OnDockingReAnchor);
|
||||
|
||||
SubscribeLocalEvent<DockingComponent, GetVerbsEvent<InteractionVerb>>(OnVerb);
|
||||
SubscribeLocalEvent<DockingComponent, BeforeDoorAutoCloseEvent>(OnAutoClose);
|
||||
|
||||
// Yes this isn't in shuttle console; it may be used by other systems technically.
|
||||
// in which case I would also add their subs here.
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, AutodockRequestMessage>(OnRequestAutodock);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, StopAutodockRequestMessage>(OnRequestStopAutodock);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, UndockRequestMessage>(OnRequestUndock);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
UpdateAutodock();
|
||||
}
|
||||
|
||||
private void OnAutoClose(EntityUid uid, DockingComponent component, BeforeDoorAutoCloseEvent args)
|
||||
|
|
@ -43,56 +61,6 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnVerb(EntityUid uid, DockingComponent component, GetVerbsEvent<InteractionVerb> args)
|
||||
{
|
||||
if (!args.CanInteract ||
|
||||
!args.CanAccess) return;
|
||||
|
||||
InteractionVerb? verb;
|
||||
|
||||
// TODO: Have it open the UI and have the UI do this.
|
||||
if (!component.Docked &&
|
||||
TryComp(uid, out PhysicsComponent? body) &&
|
||||
TryComp(uid, out TransformComponent? xform))
|
||||
{
|
||||
DockingComponent? otherDock = null;
|
||||
|
||||
if (component.Enabled)
|
||||
otherDock = GetDockable(body, xform);
|
||||
|
||||
verb = new InteractionVerb
|
||||
{
|
||||
Disabled = otherDock == null,
|
||||
Text = Loc.GetString("docking-component-dock"),
|
||||
Act = () =>
|
||||
{
|
||||
if (otherDock == null) return;
|
||||
TryDock(component, otherDock);
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (component.Docked)
|
||||
{
|
||||
verb = new InteractionVerb
|
||||
{
|
||||
Disabled = !component.Docked,
|
||||
Text = Loc.GetString("docking-component-undock"),
|
||||
Act = () =>
|
||||
{
|
||||
if (component.DockedWith == null || !component.Enabled) return;
|
||||
|
||||
Undock(component);
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private DockingComponent? GetDockable(PhysicsComponent body, TransformComponent dockingXform)
|
||||
{
|
||||
// Did you know Saltern is the most dockable station?
|
||||
|
|
@ -105,12 +73,9 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
var transform = body.GetTransform();
|
||||
var dockingFixture = _fixtureSystem.GetFixtureOrNull(body, DockingFixture);
|
||||
|
||||
// Happens if no power or whatever
|
||||
if (dockingFixture == null)
|
||||
{
|
||||
DebugTools.Assert(false);
|
||||
Logger.ErrorS("docking", $"Found null fixture on {(body).Owner}");
|
||||
return null;
|
||||
}
|
||||
|
||||
Box2? aabb = null;
|
||||
|
||||
|
|
@ -142,7 +107,7 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
if (otherDockingFixture == null)
|
||||
{
|
||||
DebugTools.Assert(false);
|
||||
Logger.ErrorS("docking", $"Found null docking fixture on {ent}");
|
||||
_sawmill.Error($"Found null docking fixture on {ent}");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +146,7 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
!TryComp(dockBUid, out DockingComponent? dockB))
|
||||
{
|
||||
DebugTools.Assert(false);
|
||||
Logger.Error("docking", $"Tried to cleanup {dockA.Owner} but not docked?");
|
||||
_sawmill.Error($"Tried to cleanup {dockA.Owner} but not docked?");
|
||||
|
||||
dockA.DockedWith = null;
|
||||
if (dockA.DockJoint != null)
|
||||
|
|
@ -247,6 +212,8 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
{
|
||||
DisableDocking(uid, component);
|
||||
}
|
||||
|
||||
_console.RefreshShuttleConsoles();
|
||||
}
|
||||
|
||||
private void OnDockingReAnchor(EntityUid uid, DockingComponent component, ref ReAnchorEvent args)
|
||||
|
|
@ -257,6 +224,7 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
|
||||
Undock(component);
|
||||
Dock(component, other);
|
||||
_console.RefreshShuttleConsoles();
|
||||
}
|
||||
|
||||
private void OnPowerChange(EntityUid uid, DockingComponent component, PowerChangedEvent args)
|
||||
|
|
@ -329,12 +297,11 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
/// </summary>
|
||||
public void Dock(DockingComponent dockA, DockingComponent dockB)
|
||||
{
|
||||
Logger.DebugS("docking", $"Docking between {dockA.Owner} and {dockB.Owner}");
|
||||
_sawmill.Debug($"Docking between {dockA.Owner} and {dockB.Owner}");
|
||||
|
||||
// https://gamedev.stackexchange.com/questions/98772/b2distancejoint-with-frequency-equal-to-0-vs-b2weldjoint
|
||||
|
||||
// We could also potentially use a prismatic joint? Depending if we want clamps that can extend or whatever
|
||||
|
||||
var dockAXform = EntityManager.GetComponent<TransformComponent>(dockA.Owner);
|
||||
var dockBXform = EntityManager.GetComponent<TransformComponent>(dockB.Owner);
|
||||
|
||||
|
|
@ -397,10 +364,7 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
EntityManager.EventBus.RaiseEvent(EventSource.Local, msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to dock 2 ports together and will return early if it's not possible.
|
||||
/// </summary>
|
||||
private void TryDock(DockingComponent dockA, DockingComponent dockB)
|
||||
private bool CanDock(DockingComponent dockA, DockingComponent dockB)
|
||||
{
|
||||
if (!TryComp(dockA.Owner, out PhysicsComponent? bodyA) ||
|
||||
!TryComp(dockB.Owner, out PhysicsComponent? bodyB) ||
|
||||
|
|
@ -409,7 +373,7 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
dockA.DockedWith != null ||
|
||||
dockB.DockedWith != null)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var fixtureA = _fixtureSystem.GetFixtureOrNull(bodyA, DockingFixture);
|
||||
|
|
@ -417,7 +381,7 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
|
||||
if (fixtureA == null || fixtureB == null)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
var transformA = bodyA.GetTransform();
|
||||
|
|
@ -441,7 +405,15 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
if (intersect) break;
|
||||
}
|
||||
|
||||
if (!intersect) return;
|
||||
return intersect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to dock 2 ports together and will return early if it's not possible.
|
||||
/// </summary>
|
||||
private void TryDock(DockingComponent dockA, DockingComponent dockB)
|
||||
{
|
||||
if (!CanDock(dockA, dockB)) return;
|
||||
|
||||
Dock(dockA, dockB);
|
||||
}
|
||||
|
|
@ -451,7 +423,7 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
if (dock.DockedWith == null)
|
||||
{
|
||||
DebugTools.Assert(false);
|
||||
Logger.ErrorS("docking", $"Tried to undock {(dock).Owner} but not docked with anything?");
|
||||
_sawmill.Error($"Tried to undock {(dock).Owner} but not docked with anything?");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -467,33 +439,12 @@ namespace Content.Server.Shuttles.EntitySystems
|
|||
_doorSystem.TryClose(doorB.Owner, doorB);
|
||||
}
|
||||
|
||||
// Could maybe give the shuttle a light push away, or at least if there's no other docks left?
|
||||
var recentlyDocked = EnsureComp<RecentlyDockedComponent>(dock.Owner);
|
||||
recentlyDocked.LastDocked = dock.DockedWith.Value;
|
||||
recentlyDocked = EnsureComp<RecentlyDockedComponent>(dock.DockedWith.Value);
|
||||
recentlyDocked.LastDocked = dock.DockedWith.Value;
|
||||
|
||||
Cleanup(dock);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever 2 airlocks dock.
|
||||
/// </summary>
|
||||
public sealed class DockEvent : EntityEventArgs
|
||||
{
|
||||
public DockingComponent DockA = default!;
|
||||
public DockingComponent DockB = default!;
|
||||
|
||||
public EntityUid GridAUid = default!;
|
||||
public EntityUid GridBUid = default!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised whenever 2 grids undock.
|
||||
/// </summary>
|
||||
public sealed class UndockEvent : EntityEventArgs
|
||||
{
|
||||
public DockingComponent DockA = default!;
|
||||
public DockingComponent DockB = default!;
|
||||
|
||||
public EntityUid GridAUid = default!;
|
||||
public EntityUid GridBUid = default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
Normal file
29
Content.Server/Shuttles/Systems/RadarConsoleSystem.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Shuttles.Systems;
|
||||
|
||||
public sealed class RadarConsoleSystem : SharedRadarConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<RadarConsoleComponent, ComponentStartup>(OnRadarStartup);
|
||||
}
|
||||
|
||||
private void OnRadarStartup(EntityUid uid, RadarConsoleComponent component, ComponentStartup args)
|
||||
{
|
||||
UpdateState(component);
|
||||
}
|
||||
|
||||
protected override void UpdateState(RadarConsoleComponent component)
|
||||
{
|
||||
var radarState = new RadarConsoleBoundInterfaceState(component.MaxRange, component.Owner, new List<DockingInterfaceState>());
|
||||
_uiSystem.GetUiOrNull(component.Owner, RadarConsoleUiKey.Key)?.SetState(radarState);
|
||||
}
|
||||
}
|
||||
337
Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
Normal file
337
Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.Events;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Shuttles.BUIStates;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Shuttles.Events;
|
||||
using Content.Shared.Shuttles.Systems;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Shuttles.Systems
|
||||
{
|
||||
public sealed class ShuttleConsoleSystem : SharedShuttleConsoleSystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tags = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, ComponentShutdown>(OnConsoleShutdown);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, PowerChangedEvent>(OnConsolePowerChange);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChange);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, ActivatableUIOpenAttemptEvent>(OnConsoleUIOpenAttempt);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, ShuttleModeRequestMessage>(OnModeRequest);
|
||||
SubscribeLocalEvent<ShuttleConsoleComponent, BoundUIClosedEvent>(OnConsoleUIClose);
|
||||
SubscribeLocalEvent<DockEvent>(OnDock);
|
||||
SubscribeLocalEvent<UndockEvent>(OnUndock);
|
||||
|
||||
SubscribeLocalEvent<PilotComponent, MoveEvent>(HandlePilotMove);
|
||||
SubscribeLocalEvent<PilotComponent, ComponentGetState>(OnGetState);
|
||||
}
|
||||
|
||||
private void OnDock(DockEvent ev)
|
||||
{
|
||||
RefreshShuttleConsoles();
|
||||
}
|
||||
|
||||
private void OnUndock(UndockEvent ev)
|
||||
{
|
||||
RefreshShuttleConsoles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes all of the data for shuttle consoles.
|
||||
/// </summary>
|
||||
public void RefreshShuttleConsoles()
|
||||
{
|
||||
// TODO: Should really call this per shuttle in some instances.
|
||||
var docks = GetAllDocks();
|
||||
|
||||
foreach (var comp in EntityQuery<ShuttleConsoleComponent>(true))
|
||||
{
|
||||
UpdateState(comp, docks);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop piloting if the window is closed.
|
||||
/// </summary>
|
||||
private void OnConsoleUIClose(EntityUid uid, ShuttleConsoleComponent component, BoundUIClosedEvent args)
|
||||
{
|
||||
if ((ShuttleConsoleUiKey) args.UiKey != ShuttleConsoleUiKey.Key ||
|
||||
args.Session.AttachedEntity is not {} user) return;
|
||||
|
||||
// In case they D/C should still clean them up.
|
||||
foreach (var comp in EntityQuery<AutoDockComponent>(true))
|
||||
{
|
||||
comp.Requesters.Remove(user);
|
||||
}
|
||||
|
||||
RemovePilot(user);
|
||||
}
|
||||
|
||||
private void OnConsoleUIOpenAttempt(EntityUid uid, ShuttleConsoleComponent component, ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
if (!TryPilot(args.User, uid))
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnConsoleAnchorChange(EntityUid uid, ShuttleConsoleComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
UpdateState(component);
|
||||
}
|
||||
|
||||
private void OnConsolePowerChange(EntityUid uid, ShuttleConsoleComponent component, PowerChangedEvent args)
|
||||
{
|
||||
UpdateState(component);
|
||||
}
|
||||
|
||||
private bool TryPilot(EntityUid user, EntityUid uid)
|
||||
{
|
||||
if (!_tags.HasTag(user, "CanPilot") ||
|
||||
!TryComp<ShuttleConsoleComponent>(uid, out var component) ||
|
||||
!this.IsPowered(uid, EntityManager) ||
|
||||
!Transform(uid).Anchored ||
|
||||
!_blocker.CanInteract(user, uid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pilotComponent = EntityManager.EnsureComponent<PilotComponent>(user);
|
||||
var console = pilotComponent.Console;
|
||||
|
||||
if (console != null)
|
||||
{
|
||||
RemovePilot(pilotComponent);
|
||||
|
||||
if (console == component)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
AddPilot(user, component);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, PilotComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new PilotComponentState(component.Console?.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client is requesting a change in the shuttle's driving mode.
|
||||
/// </summary>
|
||||
private void OnModeRequest(EntityUid uid, ShuttleConsoleComponent component, ShuttleModeRequestMessage args)
|
||||
{
|
||||
if (args.Session.AttachedEntity is not { } player ||
|
||||
!TryComp<PilotComponent>(player, out var pilot) ||
|
||||
!TryComp<TransformComponent>(player, out var xform) ||
|
||||
pilot.Console is not ShuttleConsoleComponent console) return;
|
||||
|
||||
if (!console.SubscribedPilots.Contains(pilot) ||
|
||||
!TryComp<ShuttleComponent>(xform.GridEntityId, out var shuttle)) return;
|
||||
|
||||
SetShuttleMode(args.Mode, console, shuttle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the shuttle's movement mode. Does minimal revalidation.
|
||||
/// </summary>
|
||||
private void SetShuttleMode(ShuttleMode mode, ShuttleConsoleComponent consoleComponent,
|
||||
ShuttleComponent shuttleComponent, TransformComponent? consoleXform = null)
|
||||
{
|
||||
// Re-validate
|
||||
if (!this.IsPowered(consoleComponent.Owner, EntityManager) ||
|
||||
!Resolve(consoleComponent.Owner, ref consoleXform) ||
|
||||
!consoleXform.Anchored ||
|
||||
consoleXform.GridID != Transform(shuttleComponent.Owner).GridID)
|
||||
{
|
||||
UpdateState(consoleComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
shuttleComponent.Mode = mode;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case ShuttleMode.Strafing:
|
||||
break;
|
||||
case ShuttleMode.Cruise:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
UpdateState(consoleComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the position and angle of all dockingcomponents.
|
||||
/// </summary>
|
||||
private List<DockingInterfaceState> GetAllDocks()
|
||||
{
|
||||
// TODO: NEED TO MAKE SURE THIS UPDATES ON ANCHORING CHANGES!
|
||||
var result = new List<DockingInterfaceState>();
|
||||
|
||||
foreach (var (comp, xform) in EntityQuery<DockingComponent, TransformComponent>(true))
|
||||
{
|
||||
if (xform.ParentUid != xform.GridUid) continue;
|
||||
|
||||
var state = new DockingInterfaceState()
|
||||
{
|
||||
Coordinates = xform.Coordinates,
|
||||
Angle = xform.LocalRotation,
|
||||
Entity = comp.Owner,
|
||||
Connected = comp.Docked,
|
||||
};
|
||||
result.Add(state);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void UpdateState(ShuttleConsoleComponent component, List<DockingInterfaceState>? docks = null)
|
||||
{
|
||||
TryComp<RadarConsoleComponent>(component.Owner, out var radar);
|
||||
var range = radar?.MaxRange ?? 0f;
|
||||
|
||||
TryComp<ShuttleComponent>(Transform(component.Owner).GridUid, out var shuttle);
|
||||
var mode = shuttle?.Mode ?? ShuttleMode.Cruise;
|
||||
|
||||
docks ??= GetAllDocks();
|
||||
|
||||
_ui.GetUiOrNull(component.Owner, ShuttleConsoleUiKey.Key)
|
||||
?.SetState(new ShuttleConsoleBoundInterfaceState(
|
||||
mode,
|
||||
range,
|
||||
component.Owner,
|
||||
docks));
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var toRemove = new RemQueue<PilotComponent>();
|
||||
|
||||
foreach (var comp in EntityManager.EntityQuery<PilotComponent>())
|
||||
{
|
||||
if (comp.Console == null) continue;
|
||||
|
||||
if (!_blocker.CanInteract(comp.Owner, comp.Console.Owner))
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var comp in toRemove)
|
||||
{
|
||||
RemovePilot(comp);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If pilot is moved then we'll stop them from piloting.
|
||||
/// </summary>
|
||||
private void HandlePilotMove(EntityUid uid, PilotComponent component, ref MoveEvent args)
|
||||
{
|
||||
if (component.Console == null || component.Position == null)
|
||||
{
|
||||
DebugTools.Assert(component.Position == null && component.Console == null);
|
||||
EntityManager.RemoveComponent<PilotComponent>(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.NewPosition.TryDistance(EntityManager, component.Position.Value, out var distance) &&
|
||||
distance < PilotComponent.BreakDistance) return;
|
||||
|
||||
RemovePilot(component);
|
||||
}
|
||||
|
||||
protected override void HandlePilotShutdown(EntityUid uid, PilotComponent component, ComponentShutdown args)
|
||||
{
|
||||
base.HandlePilotShutdown(uid, component, args);
|
||||
RemovePilot(component);
|
||||
}
|
||||
|
||||
private void OnConsoleShutdown(EntityUid uid, ShuttleConsoleComponent component, ComponentShutdown args)
|
||||
{
|
||||
ClearPilots(component);
|
||||
}
|
||||
|
||||
public void AddPilot(EntityUid entity, ShuttleConsoleComponent component)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent) ||
|
||||
component.SubscribedPilots.Contains(pilotComponent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<SharedEyeComponent>(entity, out var eye))
|
||||
{
|
||||
eye.Zoom = component.Zoom;
|
||||
}
|
||||
|
||||
component.SubscribedPilots.Add(pilotComponent);
|
||||
|
||||
_alertsSystem.ShowAlert(entity, AlertType.PilotingShuttle);
|
||||
|
||||
pilotComponent.Console = component;
|
||||
ActionBlockerSystem.UpdateCanMove(entity);
|
||||
pilotComponent.Position = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
|
||||
Dirty(pilotComponent);
|
||||
}
|
||||
|
||||
public void RemovePilot(PilotComponent pilotComponent)
|
||||
{
|
||||
var console = pilotComponent.Console;
|
||||
|
||||
if (console is not ShuttleConsoleComponent helmsman) return;
|
||||
|
||||
pilotComponent.Console = null;
|
||||
pilotComponent.Position = null;
|
||||
|
||||
if (TryComp<SharedEyeComponent>(pilotComponent.Owner, out var eye))
|
||||
{
|
||||
eye.Zoom = new(1.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (!helmsman.SubscribedPilots.Remove(pilotComponent)) return;
|
||||
|
||||
_alertsSystem.ClearAlert(pilotComponent.Owner, AlertType.PilotingShuttle);
|
||||
|
||||
pilotComponent.Owner.PopupMessage(Loc.GetString("shuttle-pilot-end"));
|
||||
|
||||
if (pilotComponent.LifeStage < ComponentLifeStage.Stopping)
|
||||
EntityManager.RemoveComponent<PilotComponent>(pilotComponent.Owner);
|
||||
}
|
||||
|
||||
public void RemovePilot(EntityUid entity)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(entity, out PilotComponent? pilotComponent)) return;
|
||||
|
||||
RemovePilot(pilotComponent);
|
||||
}
|
||||
|
||||
public void ClearPilots(ShuttleConsoleComponent component)
|
||||
{
|
||||
while (component.SubscribedPilots.TryGetValue(0, out var pilot))
|
||||
{
|
||||
RemovePilot(pilot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ using JetBrains.Annotations;
|
|||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Physics;
|
||||
|
||||
namespace Content.Server.Shuttles.EntitySystems
|
||||
namespace Content.Server.Shuttles.Systems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class ShuttleSystem : EntitySystem
|
||||
|
|
@ -2,7 +2,7 @@ using Content.Server.Shuttles.Components;
|
|||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
|
||||
namespace Content.Server.Shuttles.EntitySystems;
|
||||
namespace Content.Server.Shuttles.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes anything with <see cref="SpaceGarbageComponent"/> that has a cross-grid collision with a static body.
|
||||
|
|
@ -9,8 +9,8 @@ using Content.Shared.Examine;
|
|||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Temperature;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using Content.Shared.Temperature;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
|
|
@ -18,7 +18,7 @@ using Robust.Shared.Physics.Collision.Shapes;
|
|||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Shuttles.EntitySystems
|
||||
namespace Content.Server.Shuttles.Systems
|
||||
{
|
||||
public sealed class ThrusterSystem : EntitySystem
|
||||
{
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
using System.Threading;
|
||||
using Content.Server.Singularity.EntitySystems;
|
||||
using Content.Shared.Singularity.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Content.Server.Singularity.Components
|
||||
{
|
||||
public sealed class ContainmentFieldConnection : IDisposable
|
||||
{
|
||||
public readonly ContainmentFieldGeneratorComponent Generator1;
|
||||
public readonly ContainmentFieldGeneratorComponent Generator2;
|
||||
public ContainmentFieldGeneratorComponent Generator1;
|
||||
public ContainmentFieldGeneratorComponent Generator2;
|
||||
private readonly List<EntityUid> _fields = new();
|
||||
private int _sharedEnergyPool;
|
||||
private readonly CancellationTokenSource _powerDecreaseCancellationTokenSource = new();
|
||||
|
|
@ -63,7 +65,7 @@ namespace Content.Server.Singularity.Components
|
|||
}
|
||||
|
||||
|
||||
Timer.SpawnRepeating(1000, () => { SharedEnergyPool--;}, _powerDecreaseCancellationTokenSource.Token);
|
||||
Timer.SpawnRepeating(1000, () => { SharedEnergyPool--; }, _powerDecreaseCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public bool CanRepel(SharedSingularityComponent toRepel)
|
||||
|
|
@ -82,8 +84,29 @@ namespace Content.Server.Singularity.Components
|
|||
}
|
||||
_fields.Clear();
|
||||
|
||||
Generator1.RemoveConnection(this);
|
||||
Generator2.RemoveConnection(this);
|
||||
RemoveConnection(this, Generator1);
|
||||
RemoveConnection(this, Generator2);
|
||||
}
|
||||
|
||||
public void RemoveConnection(ContainmentFieldConnection? connection, ContainmentFieldGeneratorComponent component)
|
||||
{
|
||||
if (component.Connection1?.Item2 == connection)
|
||||
{
|
||||
component.Connection1 = null;
|
||||
}
|
||||
else if (component.Connection2?.Item2 == connection)
|
||||
{
|
||||
component.Connection2 = null;
|
||||
}
|
||||
else if (connection != null)
|
||||
{
|
||||
Logger.Error("RemoveConnection called on Containmentfieldgenerator with a connection that can't be found in its connections.");
|
||||
}
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<PointLightComponent>(component.Owner, out var pointLightComponent))
|
||||
{
|
||||
bool hasAnyConnection = (component.Connection1 != null) || (component.Connection2 != null);
|
||||
pointLightComponent.Enabled = hasAnyConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ namespace Content.Server.Singularity.Components
|
|||
[ComponentReference(typeof(SharedContainmentFieldGeneratorComponent))]
|
||||
public sealed class ContainmentFieldGeneratorComponent : SharedContainmentFieldGeneratorComponent
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
private int _powerBuffer;
|
||||
|
||||
[ViewVariables]
|
||||
|
|
@ -22,153 +20,8 @@ namespace Content.Server.Singularity.Components
|
|||
set => _powerBuffer = Math.Clamp(value, 0, 6);
|
||||
}
|
||||
|
||||
public void ReceivePower(int power)
|
||||
{
|
||||
var totalPower = power + PowerBuffer;
|
||||
var powerPerConnection = totalPower / 2;
|
||||
var newBuffer = totalPower % 2;
|
||||
TryPowerConnection(ref _connection1, ref newBuffer, powerPerConnection);
|
||||
TryPowerConnection(ref _connection2, ref newBuffer, powerPerConnection);
|
||||
public Tuple<Direction, ContainmentFieldConnection>? Connection1;
|
||||
public Tuple<Direction, ContainmentFieldConnection>? Connection2;
|
||||
|
||||
PowerBuffer = newBuffer;
|
||||
}
|
||||
|
||||
private void TryPowerConnection(ref Tuple<Direction, ContainmentFieldConnection>? connectionProperty, ref int powerBuffer, int powerPerConnection)
|
||||
{
|
||||
if (connectionProperty != null)
|
||||
{
|
||||
connectionProperty.Item2.SharedEnergyPool += powerPerConnection;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (TryGenerateFieldConnection(ref connectionProperty))
|
||||
{
|
||||
connectionProperty.Item2.SharedEnergyPool += powerPerConnection;
|
||||
}
|
||||
else
|
||||
{
|
||||
powerBuffer += powerPerConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Tuple<Direction, ContainmentFieldConnection>? _connection1;
|
||||
private Tuple<Direction, ContainmentFieldConnection>? _connection2;
|
||||
|
||||
public bool CanRepel(SharedSingularityComponent toRepel) => _connection1?.Item2?.CanRepel(toRepel) == true ||
|
||||
_connection2?.Item2?.CanRepel(toRepel) == true;
|
||||
|
||||
public void OnAnchoredChanged()
|
||||
{
|
||||
if(_entMan.TryGetComponent<PhysicsComponent>(Owner, out var physicsComponent) && physicsComponent.BodyType != BodyType.Static)
|
||||
{
|
||||
_connection1?.Item2.Dispose();
|
||||
_connection2?.Item2.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsConnectedWith(ContainmentFieldGeneratorComponent comp)
|
||||
{
|
||||
|
||||
return comp == this || _connection1?.Item2.Generator1 == comp || _connection1?.Item2.Generator2 == comp ||
|
||||
_connection2?.Item2.Generator1 == comp || _connection2?.Item2.Generator2 == comp;
|
||||
}
|
||||
|
||||
public bool HasFreeConnections()
|
||||
{
|
||||
return _connection1 == null || _connection2 == null;
|
||||
}
|
||||
|
||||
private bool TryGenerateFieldConnection([NotNullWhen(true)] ref Tuple<Direction, ContainmentFieldConnection>? propertyFieldTuple)
|
||||
{
|
||||
if (propertyFieldTuple != null) return false;
|
||||
if(_entMan.TryGetComponent<PhysicsComponent>(Owner, out var physicsComponent) && physicsComponent.BodyType != BodyType.Static) return false;
|
||||
|
||||
foreach (var direction in new[] {Direction.North, Direction.East, Direction.South, Direction.West})
|
||||
{
|
||||
if (_connection1?.Item1 == direction || _connection2?.Item1 == direction) continue;
|
||||
|
||||
var dirVec = _entMan.GetComponent<TransformComponent>(Owner).WorldRotation.RotateVec(direction.ToVec());
|
||||
var ray = new CollisionRay(_entMan.GetComponent<TransformComponent>(Owner).WorldPosition, dirVec, (int) CollisionGroup.MobMask);
|
||||
var rawRayCastResults = EntitySystem.Get<SharedPhysicsSystem>().IntersectRay(_entMan.GetComponent<TransformComponent>(Owner).MapID, ray, 4.5f, Owner, false);
|
||||
|
||||
var rayCastResults = rawRayCastResults as RayCastResults[] ?? rawRayCastResults.ToArray();
|
||||
if(!rayCastResults.Any()) continue;
|
||||
|
||||
RayCastResults? closestResult = null;
|
||||
var smallestDist = 4.5f;
|
||||
foreach (var res in rayCastResults)
|
||||
{
|
||||
if (res.Distance > smallestDist) continue;
|
||||
|
||||
smallestDist = res.Distance;
|
||||
closestResult = res;
|
||||
}
|
||||
if(closestResult == null) continue;
|
||||
var ent = closestResult.Value.HitEntity;
|
||||
if (!_entMan.TryGetComponent<ContainmentFieldGeneratorComponent?>(ent, out var fieldGeneratorComponent) ||
|
||||
fieldGeneratorComponent.Owner == Owner ||
|
||||
!fieldGeneratorComponent.HasFreeConnections() ||
|
||||
IsConnectedWith(fieldGeneratorComponent) ||
|
||||
!_entMan.TryGetComponent<PhysicsComponent?>(ent, out var collidableComponent) ||
|
||||
collidableComponent.BodyType != BodyType.Static)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var connection = new ContainmentFieldConnection(this, fieldGeneratorComponent);
|
||||
propertyFieldTuple = new Tuple<Direction, ContainmentFieldConnection>(direction, connection);
|
||||
if (fieldGeneratorComponent._connection1 == null)
|
||||
{
|
||||
fieldGeneratorComponent._connection1 = new Tuple<Direction, ContainmentFieldConnection>(direction.GetOpposite(), connection);
|
||||
}
|
||||
else if (fieldGeneratorComponent._connection2 == null)
|
||||
{
|
||||
fieldGeneratorComponent._connection2 = new Tuple<Direction, ContainmentFieldConnection>(direction.GetOpposite(), connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("When trying to connect two Containmentfieldgenerators, the second one already had two connection but the check didn't catch it");
|
||||
}
|
||||
UpdateConnectionLights();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RemoveConnection(ContainmentFieldConnection? connection)
|
||||
{
|
||||
if (_connection1?.Item2 == connection)
|
||||
{
|
||||
_connection1 = null;
|
||||
UpdateConnectionLights();
|
||||
}
|
||||
else if (_connection2?.Item2 == connection)
|
||||
{
|
||||
_connection2 = null;
|
||||
UpdateConnectionLights();
|
||||
}
|
||||
else if(connection != null)
|
||||
{
|
||||
Logger.Error("RemoveConnection called on Containmentfieldgenerator with a connection that can't be found in its connections.");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateConnectionLights()
|
||||
{
|
||||
if (_entMan.TryGetComponent<PointLightComponent>(Owner, out var pointLightComponent))
|
||||
{
|
||||
bool hasAnyConnection = (_connection1 != null) || (_connection2 != null);
|
||||
pointLightComponent.Enabled = hasAnyConnection;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnRemove()
|
||||
{
|
||||
_connection1?.Item2.Dispose();
|
||||
_connection2?.Item2.Dispose();
|
||||
base.OnRemove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
using Content.Server.ParticleAccelerator.Components;
|
||||
using Content.Server.Singularity.Components;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Singularity.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Singularity.EntitySystems
|
||||
{
|
||||
|
|
@ -14,10 +19,26 @@ namespace Content.Server.Singularity.EntitySystems
|
|||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, ComponentRemove>(OnComponentRemoved);
|
||||
SubscribeLocalEvent<ContainmentFieldComponent, StartCollideEvent>(HandleFieldCollide);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, StartCollideEvent>(HandleGeneratorCollide);
|
||||
SubscribeLocalEvent<ParticleProjectileComponent, StartCollideEvent>(HandleParticleCollide);
|
||||
SubscribeLocalEvent<ContainmentFieldGeneratorComponent, AnchorStateChangedEvent>(OnAnchorChanged);
|
||||
}
|
||||
|
||||
private void OnComponentRemoved(EntityUid uid, ContainmentFieldGeneratorComponent component, ComponentRemove args)
|
||||
{
|
||||
component.Connection1?.Item2.Dispose();
|
||||
component.Connection2?.Item2.Dispose();
|
||||
}
|
||||
|
||||
private void OnAnchorChanged(EntityUid uid, ContainmentFieldGeneratorComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
if (!args.Anchored)
|
||||
{
|
||||
component.Connection1?.Item2.Dispose();
|
||||
component.Connection2?.Item2.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleParticleCollide(EntityUid uid, ParticleProjectileComponent component, StartCollideEvent args)
|
||||
|
|
@ -40,8 +61,9 @@ namespace Content.Server.Singularity.EntitySystems
|
|||
|
||||
private void HandleGeneratorCollide(EntityUid uid, ContainmentFieldGeneratorComponent component, StartCollideEvent args)
|
||||
{
|
||||
if (_tags.HasTag(args.OtherFixture.Body.Owner, "EmitterBolt")) {
|
||||
component.ReceivePower(6);
|
||||
if (_tags.HasTag(args.OtherFixture.Body.Owner, "EmitterBolt"))
|
||||
{
|
||||
ReceivePower(6, component);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,12 +76,135 @@ namespace Content.Server.Singularity.EntitySystems
|
|||
}
|
||||
}
|
||||
|
||||
private static void BodyTypeChanged(
|
||||
EntityUid uid,
|
||||
ContainmentFieldGeneratorComponent component,
|
||||
ref PhysicsBodyTypeChangedEvent args)
|
||||
public void ReceivePower(int power, ContainmentFieldGeneratorComponent component)
|
||||
{
|
||||
component.OnAnchoredChanged();
|
||||
var totalPower = power + component.PowerBuffer;
|
||||
var powerPerConnection = totalPower / 2;
|
||||
var newBuffer = totalPower % 2;
|
||||
TryPowerConnection(ref component.Connection1, ref newBuffer, powerPerConnection, component);
|
||||
TryPowerConnection(ref component.Connection2, ref newBuffer, powerPerConnection, component);
|
||||
|
||||
component.PowerBuffer = newBuffer;
|
||||
}
|
||||
|
||||
public void UpdateConnectionLights(ContainmentFieldGeneratorComponent component)
|
||||
{
|
||||
if (EntityManager.TryGetComponent<PointLightComponent>(component.Owner, out var pointLightComponent))
|
||||
{
|
||||
bool hasAnyConnection = (component.Connection1 != null) || (component.Connection2 != null);
|
||||
pointLightComponent.Enabled = hasAnyConnection;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveConnection(ContainmentFieldConnection? connection, ContainmentFieldGeneratorComponent component)
|
||||
{
|
||||
if (component.Connection1?.Item2 == connection)
|
||||
{
|
||||
component.Connection1 = null;
|
||||
UpdateConnectionLights(component);
|
||||
}
|
||||
else if (component.Connection2?.Item2 == connection)
|
||||
{
|
||||
component.Connection2 = null;
|
||||
UpdateConnectionLights(component);
|
||||
}
|
||||
else if (connection != null)
|
||||
{
|
||||
Logger.Error("RemoveConnection called on Containmentfieldgenerator with a connection that can't be found in its connections.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGenerateFieldConnection([NotNullWhen(true)] ref Tuple<Direction, ContainmentFieldConnection>? propertyFieldTuple, ContainmentFieldGeneratorComponent component)
|
||||
{
|
||||
if (propertyFieldTuple != null) return false;
|
||||
if (EntityManager.TryGetComponent<TransformComponent>(component.Owner, out var xform) && !xform.Anchored) return false;
|
||||
|
||||
foreach (var direction in new[] { Direction.North, Direction.East, Direction.South, Direction.West })
|
||||
{
|
||||
if (component.Connection1?.Item1 == direction || component.Connection2?.Item1 == direction) continue;
|
||||
|
||||
var dirVec = EntityManager.GetComponent<TransformComponent>(component.Owner).WorldRotation.RotateVec(direction.ToVec());
|
||||
var ray = new CollisionRay(EntityManager.GetComponent<TransformComponent>(component.Owner).WorldPosition, dirVec, (int) CollisionGroup.MobMask);
|
||||
var rawRayCastResults = EntitySystem.Get<SharedPhysicsSystem>().IntersectRay(EntityManager.GetComponent<TransformComponent>(component.Owner).MapID, ray, 4.5f, component.Owner, false);
|
||||
|
||||
var rayCastResults = rawRayCastResults as RayCastResults[] ?? rawRayCastResults.ToArray();
|
||||
if (!rayCastResults.Any()) continue;
|
||||
|
||||
RayCastResults? closestResult = null;
|
||||
var smallestDist = 4.5f;
|
||||
foreach (var res in rayCastResults)
|
||||
{
|
||||
if (res.Distance > smallestDist) continue;
|
||||
|
||||
smallestDist = res.Distance;
|
||||
closestResult = res;
|
||||
}
|
||||
if (closestResult == null) continue;
|
||||
var ent = closestResult.Value.HitEntity;
|
||||
if (!EntityManager.TryGetComponent<ContainmentFieldGeneratorComponent?>(ent, out var fieldGeneratorComponent) ||
|
||||
fieldGeneratorComponent.Owner == component.Owner ||
|
||||
!HasFreeConnections(fieldGeneratorComponent) ||
|
||||
IsConnectedWith(component, fieldGeneratorComponent) ||
|
||||
!EntityManager.TryGetComponent<PhysicsComponent?>(ent, out var collidableComponent) ||
|
||||
collidableComponent.BodyType != BodyType.Static)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var connection = new ContainmentFieldConnection(component, fieldGeneratorComponent);
|
||||
propertyFieldTuple = new Tuple<Direction, ContainmentFieldConnection>(direction, connection);
|
||||
if (fieldGeneratorComponent.Connection1 == null)
|
||||
{
|
||||
fieldGeneratorComponent.Connection1 = new Tuple<Direction, ContainmentFieldConnection>(direction.GetOpposite(), connection);
|
||||
}
|
||||
else if (fieldGeneratorComponent.Connection2 == null)
|
||||
{
|
||||
fieldGeneratorComponent.Connection2 = new Tuple<Direction, ContainmentFieldConnection>(direction.GetOpposite(), connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("When trying to connect two Containmentfieldgenerators, the second one already had two connection but the check didn't catch it");
|
||||
}
|
||||
UpdateConnectionLights(component);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsConnectedWith(ContainmentFieldGeneratorComponent comp, ContainmentFieldGeneratorComponent otherComp)
|
||||
{
|
||||
return otherComp == comp || comp.Connection1?.Item2.Generator1 == otherComp || comp.Connection1?.Item2.Generator2 == otherComp ||
|
||||
comp.Connection2?.Item2.Generator1 == otherComp || comp.Connection2?.Item2.Generator2 == otherComp;
|
||||
}
|
||||
|
||||
public void TryPowerConnection(ref Tuple<Direction, ContainmentFieldConnection>? connectionProperty, ref int powerBuffer, int powerPerConnection, ContainmentFieldGeneratorComponent component)
|
||||
{
|
||||
if (connectionProperty != null)
|
||||
{
|
||||
connectionProperty.Item2.SharedEnergyPool += powerPerConnection;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (TryGenerateFieldConnection(ref connectionProperty, component))
|
||||
{
|
||||
connectionProperty.Item2.SharedEnergyPool += powerPerConnection;
|
||||
}
|
||||
else
|
||||
{
|
||||
powerBuffer += powerPerConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanRepel(SharedSingularityComponent toRepel, ContainmentFieldGeneratorComponent component) => component.Connection1?.Item2?.CanRepel(toRepel) == true ||
|
||||
component.Connection2?.Item2?.CanRepel(toRepel) == true;
|
||||
|
||||
public bool HasFreeConnections(ContainmentFieldGeneratorComponent component)
|
||||
{
|
||||
return component.Connection1 == null || component.Connection2 == null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Content.Server.Singularity.EntitySystems
|
|||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly PVSOverrideSystem _pvs = default!;
|
||||
|
||||
[Dependency] private readonly ContainmentFieldGeneratorSystem _fieldGeneratorSystem = default!;
|
||||
/// <summary>
|
||||
/// How much energy the singulo gains from destroying a tile.
|
||||
/// </summary>
|
||||
|
|
@ -132,7 +132,7 @@ namespace Content.Server.Singularity.EntitySystems
|
|||
!EntityManager.HasComponent<GhostComponent>(entity) &&
|
||||
(component.Level > 4 ||
|
||||
!EntityManager.HasComponent<ContainmentFieldComponent>(entity) &&
|
||||
!(EntityManager.TryGetComponent<ContainmentFieldGeneratorComponent>(entity, out var containFieldGen) && containFieldGen.CanRepel(component)));
|
||||
!(EntityManager.TryGetComponent<ContainmentFieldGeneratorComponent>(entity, out var containFieldGen) && _fieldGeneratorSystem.CanRepel(component, containFieldGen)));
|
||||
}
|
||||
|
||||
private void HandleDestroy(ServerSingularityComponent component, EntityUid entity)
|
||||
|
|
|
|||
15
Content.Server/Speech/Components/MobsterAccentComponent.cs
Normal file
15
Content.Server/Speech/Components/MobsterAccentComponent.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Nyehh, my gabagool, see?
|
||||
/// Etc etc.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class MobsterAccentComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Do you make all the rules?
|
||||
/// </summary>
|
||||
[DataField("isBoss")]
|
||||
public bool IsBoss = true;
|
||||
}
|
||||
89
Content.Server/Speech/EntitySystems/MobsterAccentSystem.cs
Normal file
89
Content.Server/Speech/EntitySystems/MobsterAccentSystem.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Server.Speech.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class MobsterAccentSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private static readonly Dictionary<string, string> DirectReplacements = new()
|
||||
{
|
||||
{ "let me", "lemme" },
|
||||
{ "should", "oughta" },
|
||||
{ "the", "da" },
|
||||
{ "kill", "whack" },
|
||||
{ "murder", "whack" },
|
||||
{ "dead", "sleepin' with da fishies"},
|
||||
{ "hey", "ey'o" },
|
||||
{ "rules", "roolz" },
|
||||
{ "you", "yous" },
|
||||
{ "have to", "gotta" },
|
||||
{ "here", "'ere" }
|
||||
};
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MobsterAccentComponent, AccentGetEvent>(OnAccentGet);
|
||||
}
|
||||
|
||||
public string Accentuate(string message, MobsterAccentComponent component)
|
||||
{
|
||||
// Order:
|
||||
// Do text manipulations first
|
||||
// Then prefix/suffix funnyies
|
||||
|
||||
var msg = message;
|
||||
|
||||
foreach (var (first, replace) in DirectReplacements)
|
||||
{
|
||||
msg = msg.Replace(first, replace, true, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// thinking -> thinkin'
|
||||
msg = Regex.Replace(msg, @"ing(?!\w)", "in'", RegexOptions.IgnoreCase);
|
||||
|
||||
// or -> uh and ar -> ah in the middle of words (fuhget, tahget)
|
||||
msg = Regex.Replace(msg, @"(?<=\w)or(?=\w)", "uh", RegexOptions.IgnoreCase);
|
||||
msg = Regex.Replace(msg, @"(?<=\w)ar(?=\w)", "ah", RegexOptions.IgnoreCase);
|
||||
|
||||
// Prefix
|
||||
if (_random.Prob(0.15f))
|
||||
{
|
||||
var pick = _random.Next(1, 2);
|
||||
|
||||
// Reverse sanitize capital
|
||||
msg = msg[0].ToString().ToLower() + msg.Remove(0, 1);
|
||||
msg = Loc.GetString($"accent-mobster-prefix-{pick}") + " " + msg;
|
||||
}
|
||||
|
||||
// Sanitize capital again, in case we substituted a word that should be capitalized
|
||||
msg = msg[0].ToString().ToUpper() + msg.Remove(0, 1);
|
||||
|
||||
// Suffixes
|
||||
if (_random.Prob(0.4f))
|
||||
{
|
||||
if (component.IsBoss)
|
||||
{
|
||||
var pick = _random.Next(1, 4);
|
||||
msg += Loc.GetString($"accent-mobster-suffix-boss-{pick}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var pick = _random.Next(1, 3);
|
||||
msg += Loc.GetString($"accent-mobster-suffix-minion-{pick}");
|
||||
}
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
private void OnAccentGet(EntityUid uid, MobsterAccentComponent component, AccentGetEvent args)
|
||||
{
|
||||
args.Message = Accentuate(args.Message, component);
|
||||
}
|
||||
}
|
||||
|
|
@ -182,24 +182,21 @@ public sealed class StationSystem : EntitySystem
|
|||
|
||||
if (stationConfig is not null && name is null)
|
||||
{
|
||||
metaData.EntityName = GenerateStationName(stationConfig);
|
||||
name = GenerateStationName(stationConfig);
|
||||
}
|
||||
else if (name is not null)
|
||||
{
|
||||
metaData.EntityName = name;
|
||||
}
|
||||
else
|
||||
else if (name is null)
|
||||
{
|
||||
_sawmill.Error($"When setting up station {station}, was unable to find a valid name in the config and no name was provided.");
|
||||
metaData.EntityName = "unnamed station";
|
||||
name = "unnamed station";
|
||||
}
|
||||
|
||||
metaData.EntityName = name;
|
||||
RaiseLocalEvent(new StationInitializedEvent(station));
|
||||
_sawmill.Info($"Set up station {metaData.EntityName} ({station}).");
|
||||
|
||||
foreach (var grid in gridIds ?? Array.Empty<EntityUid>())
|
||||
{
|
||||
AddGridToStation(station, grid, null, data);
|
||||
AddGridToStation(station, grid, null, data, name);
|
||||
}
|
||||
|
||||
return station;
|
||||
|
|
@ -213,13 +210,16 @@ public sealed class StationSystem : EntitySystem
|
|||
/// <param name="gridComponent">Resolve pattern, grid component of mapGrid.</param>
|
||||
/// <param name="stationData">Resolve pattern, station data component of station.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when mapGrid or station are not a grid or station, respectively.</exception>
|
||||
public void AddGridToStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null)
|
||||
public void AddGridToStation(EntityUid station, EntityUid mapGrid, IMapGridComponent? gridComponent = null, StationDataComponent? stationData = null, string? name = null)
|
||||
{
|
||||
if (!Resolve(mapGrid, ref gridComponent))
|
||||
throw new ArgumentException("Tried to initialize a station on a non-grid entity!", nameof(mapGrid));
|
||||
if (!Resolve(station, ref stationData))
|
||||
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
MetaData(mapGrid).EntityName = name;
|
||||
|
||||
var stationMember = AddComp<StationMemberComponent>(mapGrid);
|
||||
stationMember.Station = station;
|
||||
stationData.Grids.Add(gridComponent.Owner);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ public sealed class DiseaseOutbreak : StationEvent
|
|||
public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/outbreak7.ogg");
|
||||
protected override float EndAfter => 1.0f;
|
||||
|
||||
public override bool AnnounceEvent => false;
|
||||
|
||||
/// <summary>
|
||||
/// Finds 2-5 random, alive entities that can host diseases
|
||||
/// and gives them a randomly selected disease.
|
||||
|
|
@ -79,6 +81,8 @@ public sealed class DiseaseOutbreak : StationEvent
|
|||
stationsToNotify.Add((EntityUid) station);
|
||||
}
|
||||
|
||||
if (!AnnounceEvent)
|
||||
return;
|
||||
foreach (var station in stationsToNotify)
|
||||
{
|
||||
chatSystem.DispatchStationAnnouncement(station, Loc.GetString("station-event-disease-outbreak-announcement"),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Content.Server.StationEvents.Events
|
|||
protected override string EndAnnouncement => Loc.GetString("station-event-gas-leak-end-announcement");
|
||||
|
||||
private static readonly Gas[] LeakableGases = {
|
||||
Gas.Miasma,
|
||||
Gas.Plasma,
|
||||
Gas.Tritium,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public sealed class KudzuGrowth : StationEvent
|
|||
// Give crew at least 9 minutes to either have it gone, or to suffer another event. Kudzu is not actually required to be dead for another event to roll.
|
||||
protected override float EndAfter => 60*4;
|
||||
|
||||
public override bool AnnounceEvent => false;
|
||||
|
||||
private EntityUid _targetGrid;
|
||||
|
||||
private Vector2i _targetTile;
|
||||
|
|
|
|||
52
Content.Server/StationEvents/Events/MouseMigration.cs
Normal file
52
Content.Server/StationEvents/Events/MouseMigration.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.Linq;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.StationEvents.Events;
|
||||
|
||||
public sealed class MouseMigration : StationEvent
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public static List<string> SpawnedPrototypeChoices = new List<string>() //we double up for that ez fake probability
|
||||
{"MobMouse", "MobMouse1", "MobMouse2", "MobRatServant"};
|
||||
|
||||
public override string Name => "MouseMigration";
|
||||
|
||||
public override string? StartAnnouncement =>
|
||||
Loc.GetString("station-event-mouse-migration-announcement");
|
||||
|
||||
public override int EarliestStart => 30;
|
||||
|
||||
public override int MinimumPlayers => 35; //this just ensures that it doesn't spawn on lowpop maps.
|
||||
|
||||
public override float Weight => WeightLow;
|
||||
|
||||
public override int? MaxOccurrences => 1;
|
||||
|
||||
public override bool AnnounceEvent => false;
|
||||
|
||||
protected override float StartAfter => 30f;
|
||||
|
||||
protected override float EndAfter => 60;
|
||||
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
var spawnLocations = _entityManager.EntityQuery<VentCritterSpawnLocationComponent, TransformComponent>().ToList();
|
||||
_random.Shuffle(spawnLocations);
|
||||
|
||||
var spawnAmount = _random.Next(7, 15); // A small colony of critters.
|
||||
|
||||
for (int i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
|
||||
{
|
||||
var spawnChoice = _random.Pick(SpawnedPrototypeChoices);
|
||||
if (_random.Prob(0.01f) || i == 0) //small chance for multiple, but always at least 1
|
||||
spawnChoice = "MobRatKing";
|
||||
|
||||
_entityManager.SpawnEntity(spawnChoice, spawnLocations[i].Item2.Coordinates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
using Content.Server.Radiation;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Coordinates;
|
||||
using Content.Shared.Sound;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.StationEvents.Events
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class RadiationStorm : StationEvent
|
||||
{
|
||||
// Based on Goonstation style radiation storm with some TG elements (announcer, etc.)
|
||||
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
|
||||
private StationSystem _stationSystem = default!;
|
||||
|
||||
public override string Name => "RadiationStorm";
|
||||
public override string StartAnnouncement => Loc.GetString("station-event-radiation-storm-start-announcement");
|
||||
protected override string EndAnnouncement => Loc.GetString("station-event-radiation-storm-end-announcement");
|
||||
public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/radiation.ogg");
|
||||
protected override float StartAfter => 10.0f;
|
||||
|
||||
// Event specific details
|
||||
private float _timeUntilPulse;
|
||||
private const float MinPulseDelay = 0.2f;
|
||||
private const float MaxPulseDelay = 0.8f;
|
||||
private EntityUid _target = EntityUid.Invalid;
|
||||
|
||||
private void ResetTimeUntilPulse()
|
||||
{
|
||||
_timeUntilPulse = _robustRandom.NextFloat() * (MaxPulseDelay - MinPulseDelay) + MinPulseDelay;
|
||||
}
|
||||
|
||||
public override void Announce()
|
||||
{
|
||||
base.Announce();
|
||||
EndAfter = _robustRandom.Next(30, 80) + StartAfter; // We want to be forgiving about the radstorm.
|
||||
}
|
||||
|
||||
public override void Startup()
|
||||
{
|
||||
_entityManager.EntitySysManager.Resolve(ref _stationSystem);
|
||||
ResetTimeUntilPulse();
|
||||
|
||||
if (_stationSystem.Stations.Count == 0)
|
||||
{
|
||||
Running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_target = _robustRandom.Pick(_stationSystem.Stations);
|
||||
base.Startup();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!Started || !Running) return;
|
||||
if (_target.Valid == false)
|
||||
{
|
||||
Running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_timeUntilPulse -= frameTime;
|
||||
|
||||
if (_timeUntilPulse <= 0.0f)
|
||||
{
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
// Account for split stations by just randomly picking a piece of it.
|
||||
var possibleTargets = _entityManager.GetComponent<StationDataComponent>(_target).Grids;
|
||||
if (possibleTargets.Count == 0)
|
||||
{
|
||||
Running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var stationEnt = _robustRandom.Pick(possibleTargets);
|
||||
|
||||
if (!_entityManager.TryGetComponent<IMapGridComponent>(stationEnt, out var grid))
|
||||
return;
|
||||
|
||||
if (mapManager.IsGridPaused(grid.Owner))
|
||||
return;
|
||||
|
||||
SpawnPulse(grid.Grid);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnPulse(IMapGrid mapGrid)
|
||||
{
|
||||
if (!TryFindRandomGrid(mapGrid, out var coordinates))
|
||||
return;
|
||||
|
||||
var pulse = _entityManager.SpawnEntity("RadiationPulse", coordinates);
|
||||
_entityManager.GetComponent<RadiationPulseComponent>(pulse).DoPulse();
|
||||
ResetTimeUntilPulse();
|
||||
}
|
||||
|
||||
public void SpawnPulseAt(EntityCoordinates at)
|
||||
{
|
||||
var pulse = IoCManager.Resolve<IEntityManager>()
|
||||
.SpawnEntity("RadiationPulse", at);
|
||||
_entityManager.GetComponent<RadiationPulseComponent>(pulse).DoPulse();
|
||||
}
|
||||
|
||||
private bool TryFindRandomGrid(IMapGrid mapGrid, out EntityCoordinates coordinates)
|
||||
{
|
||||
if (!(mapGrid.GridEntityId).IsValid())
|
||||
{
|
||||
coordinates = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var bounds = mapGrid.LocalAABB;
|
||||
var randomX = _robustRandom.Next((int) bounds.Left, (int) bounds.Right);
|
||||
var randomY = _robustRandom.Next((int) bounds.Bottom, (int) bounds.Top);
|
||||
|
||||
coordinates = mapGrid.ToCoordinates(randomX, randomY);
|
||||
|
||||
// TODO: Need to get valid tiles? (maybe just move right if the tile we chose is invalid?)
|
||||
if (!coordinates.IsValid(_entityManager))
|
||||
{
|
||||
coordinates = default;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,11 @@ namespace Content.Server.StationEvents.Events
|
|||
/// </summary>
|
||||
public virtual int? MaxOccurrences { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the event is announced when it is run
|
||||
/// </summary>
|
||||
public virtual bool AnnounceEvent { get; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Has the startup time elapsed?
|
||||
/// </summary>
|
||||
|
|
@ -139,13 +144,13 @@ namespace Content.Server.StationEvents.Events
|
|||
IoCManager.Resolve<IAdminLogManager>()
|
||||
.Add(LogType.EventAnnounced, $"Event announce: {Name}");
|
||||
|
||||
if (StartAnnouncement != null)
|
||||
if (AnnounceEvent && StartAnnouncement != null)
|
||||
{
|
||||
var chatSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
|
||||
chatSystem.DispatchGlobalStationAnnouncement(StartAnnouncement, playDefaultSound: false, colorOverride: Color.Gold);
|
||||
}
|
||||
|
||||
if (StartAudio != null)
|
||||
if (AnnounceEvent && StartAudio != null)
|
||||
{
|
||||
SoundSystem.Play(StartAudio.GetSound(), Filter.Broadcast(), AudioParams);
|
||||
}
|
||||
|
|
@ -162,13 +167,13 @@ namespace Content.Server.StationEvents.Events
|
|||
IoCManager.Resolve<IAdminLogManager>()
|
||||
.Add(LogType.EventStopped, $"Event shutdown: {Name}");
|
||||
|
||||
if (EndAnnouncement != null)
|
||||
if (AnnounceEvent && EndAnnouncement != null)
|
||||
{
|
||||
var chatSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
|
||||
chatSystem.DispatchGlobalStationAnnouncement(EndAnnouncement, playDefaultSound: false, colorOverride: Color.Gold);
|
||||
}
|
||||
|
||||
if (EndAudio != null)
|
||||
if (AnnounceEvent && EndAudio != null)
|
||||
{
|
||||
SoundSystem.Play(EndAudio.GetSound(), Filter.Broadcast(), AudioParams);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ public sealed class VentCritters : StationEvent
|
|||
|
||||
protected override float EndAfter => 60;
|
||||
|
||||
public override bool AnnounceEvent => false;
|
||||
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
|
|
|||
|
|
@ -19,11 +19,10 @@ namespace Content.Server.StationEvents.Events
|
|||
public override string Name => "ZombieOutbreak";
|
||||
public override int EarliestStart => 50;
|
||||
public override float Weight => WeightLow / 2;
|
||||
|
||||
public override SoundSpecifier? StartAudio => new SoundPathSpecifier("/Audio/Announcements/bloblarm.ogg");
|
||||
protected override float EndAfter => 1.0f;
|
||||
|
||||
public override int? MaxOccurrences => 1;
|
||||
public override bool AnnounceEvent => false;
|
||||
|
||||
/// <summary>
|
||||
/// Finds 1-3 random, dead entities accross the station
|
||||
|
|
@ -61,6 +60,9 @@ namespace Content.Server.StationEvents.Events
|
|||
if(station == null) continue;
|
||||
stationsToNotify.Add((EntityUid) station);
|
||||
}
|
||||
|
||||
if (!AnnounceEvent)
|
||||
return;
|
||||
foreach (var station in stationsToNotify)
|
||||
{
|
||||
chatSystem.DispatchStationAnnouncement((EntityUid) station, Loc.GetString("station-event-zombie-outbreak-announcement"),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public sealed class StickySystem : EntitySystem
|
|||
|
||||
private void AddUnstickVerb(EntityUid uid, StickyComponent component, GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (component.StuckTo == null || !component.CanUnstick || !args.CanInteract)
|
||||
if (component.StuckTo == null || !component.CanUnstick || !args.CanInteract || args.Hands == null)
|
||||
return;
|
||||
|
||||
// we can't use args.CanAccess, because it stuck in another container
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Content.Server.Weapon.Melee.Components
|
|||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("arc")]
|
||||
public string Arc { get; set; } = "default";
|
||||
public string? Arc { get; set; } = "default";
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("arcwidth")]
|
||||
|
|
|
|||
|
|
@ -36,5 +36,8 @@ namespace Content.Server.Weapon.Melee.EnergySword
|
|||
|
||||
[DataField("litDamageBonus", required: true)]
|
||||
public DamageSpecifier LitDamageBonus = default!;
|
||||
|
||||
[DataField("litDisarmMalus", required: true)]
|
||||
public float litDisarmMalus = 0.6f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using Content.Shared.Light;
|
|||
using Content.Shared.Light.Component;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.Tools.Components;
|
||||
using Content.Server.CombatMode.Disarm;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
|
@ -68,6 +69,11 @@ namespace Content.Server.Weapon.Melee.EnergySword
|
|||
item.Size = 5;
|
||||
}
|
||||
|
||||
if (TryComp<DisarmMalusComponent>(comp.Owner, out var malus))
|
||||
{
|
||||
malus.Malus -= comp.litDisarmMalus;
|
||||
}
|
||||
|
||||
SoundSystem.Play(comp.DeActivateSound.GetSound(), Filter.Pvs(comp.Owner, entityManager: EntityManager), comp.Owner);
|
||||
|
||||
comp.Activated = false;
|
||||
|
|
@ -85,6 +91,11 @@ namespace Content.Server.Weapon.Melee.EnergySword
|
|||
|
||||
SoundSystem.Play(comp.ActivateSound.GetSound(), Filter.Pvs(comp.Owner, entityManager: EntityManager), comp.Owner);
|
||||
|
||||
if (TryComp<DisarmMalusComponent>(comp.Owner, out var malus))
|
||||
{
|
||||
malus.Malus += comp.litDisarmMalus;
|
||||
}
|
||||
|
||||
comp.Activated = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ namespace Content.Server.Weapon.Melee
|
|||
|
||||
private void OnWideAttack(EntityUid owner, MeleeWeaponComponent comp, WideAttackEvent args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(comp.Arc)) return;
|
||||
|
||||
args.Handled = true;
|
||||
var curTime = _gameTiming.CurTime;
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ public sealed class WiresSystem : EntitySystem
|
|||
// This is where all the wire layouts are stored.
|
||||
[ViewVariables] private readonly Dictionary<string, WireLayout> _layouts = new();
|
||||
|
||||
private const float ScrewTime = 2.5f;
|
||||
private const float ToolTime = 1f;
|
||||
private const float ScrewTime = 1f;
|
||||
private float _toolTime = 0f;
|
||||
|
||||
private static DummyWireAction _dummyWire = new DummyWireAction();
|
||||
|
||||
|
|
@ -649,28 +649,36 @@ public sealed class WiresSystem : EntitySystem
|
|||
break;
|
||||
}
|
||||
|
||||
var args = new DoAfterEventArgs(user, ToolTime, default, used)
|
||||
if (_toolTime > 0f)
|
||||
{
|
||||
NeedHand = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnUserMove = true,
|
||||
TargetFinishedEvent = new OnWireDoAfterEvent
|
||||
var args = new DoAfterEventArgs(user, _toolTime, default, used)
|
||||
{
|
||||
Target = used,
|
||||
User = user,
|
||||
Tool = toolEntity,
|
||||
Action = action,
|
||||
Id = id
|
||||
},
|
||||
TargetCancelledEvent = new OnWireDoAfterCancelEvent
|
||||
{
|
||||
Id = id
|
||||
}
|
||||
};
|
||||
_doAfter.DoAfter(args);
|
||||
NeedHand = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnUserMove = true,
|
||||
TargetFinishedEvent = new OnWireDoAfterEvent
|
||||
{
|
||||
Target = used,
|
||||
User = user,
|
||||
Tool = toolEntity,
|
||||
Action = action,
|
||||
Id = id
|
||||
},
|
||||
TargetCancelledEvent = new OnWireDoAfterCancelEvent
|
||||
{
|
||||
Id = id
|
||||
}
|
||||
};
|
||||
|
||||
wires.WiresQueue.Add(id);
|
||||
_doAfter.DoAfter(args);
|
||||
|
||||
wires.WiresQueue.Add(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateWires(used, user, toolEntity, id, action, wires);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ public sealed class GasArtifactComponent : Component
|
|||
Gas.Plasma,
|
||||
Gas.Nitrogen,
|
||||
Gas.CarbonDioxide,
|
||||
Gas.Tritium
|
||||
Gas.Tritium,
|
||||
Gas.Miasma,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue