Merge branch 'space-wizards:master' into elk-new
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using Content.Client.Atmos.UI;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
@@ -15,7 +14,12 @@ public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
|
||||
|
||||
private void OnPumpUpdate(Entity<GasPressurePumpComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (UserInterfaceSystem.TryGetOpenUi<GasPressurePumpBoundUserInterface>(ent.Owner, GasPressurePumpUiKey.Key, out var bui))
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
protected override void UpdateUi(Entity<GasPressurePumpComponent> ent)
|
||||
{
|
||||
if (UserInterfaceSystem.TryGetOpenUi(ent.Owner, GasPressurePumpUiKey.Key, out var bui))
|
||||
{
|
||||
bui.Update();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Localizations;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
@@ -12,7 +11,7 @@ namespace Content.Client.Atmos.UI;
|
||||
/// Initializes a <see cref="GasPressurePumpWindow"/> and updates it when new server messages are received.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
|
||||
public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
[ViewVariables]
|
||||
private const float MaxPressure = Atmospherics.MaxOutputPressure;
|
||||
@@ -20,10 +19,6 @@ public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
|
||||
[ViewVariables]
|
||||
private GasPressurePumpWindow? _window;
|
||||
|
||||
public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
@@ -35,7 +30,7 @@ public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
|
||||
Update();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
public override void Update()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
@@ -52,7 +47,9 @@ public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
|
||||
|
||||
private void OnToggleStatusButtonPressed()
|
||||
{
|
||||
if (_window is null) return;
|
||||
if (_window is null)
|
||||
return;
|
||||
|
||||
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Atmos.UI
|
||||
|
||||
@@ -67,8 +67,10 @@ public sealed partial class CargoSystem
|
||||
if (!Resolve(uid, ref sprite))
|
||||
return;
|
||||
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
|
||||
return;
|
||||
|
||||
_appearance.TryGetData<CargoTelepadState?>(uid, CargoTelepadVisuals.State, out var state);
|
||||
AnimationPlayerComponent? player = null;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
@@ -76,7 +78,7 @@ public sealed partial class CargoSystem
|
||||
if (_player.HasRunningAnimation(uid, TelepadBeamKey))
|
||||
return;
|
||||
_player.Stop(uid, player, TelepadIdleKey);
|
||||
_player.Play(uid, player, CargoTelepadBeamAnimation, TelepadBeamKey);
|
||||
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
|
||||
break;
|
||||
case CargoTelepadState.Unpowered:
|
||||
sprite.LayerSetVisible(CargoTelepadLayers.Beam, false);
|
||||
@@ -90,7 +92,7 @@ public sealed partial class CargoSystem
|
||||
_player.HasRunningAnimation(uid, player, TelepadBeamKey))
|
||||
return;
|
||||
|
||||
_player.Play(uid, player, CargoTelepadIdleAnimation, TelepadIdleKey);
|
||||
_player.Play((uid, player), CargoTelepadIdleAnimation, TelepadIdleKey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- General -->
|
||||
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Vocal -->
|
||||
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Hands -->
|
||||
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
@@ -1,111 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Chat.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class EmotesMenu : RadialMenu
|
||||
{
|
||||
[Dependency] private readonly EntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
|
||||
public event Action<ProtoId<EmotePrototype>>? OnPlayEmote;
|
||||
|
||||
public EmotesMenu()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var spriteSystem = _entManager.System<SpriteSystem>();
|
||||
var whitelistSystem = _entManager.System<EntityWhitelistSystem>();
|
||||
|
||||
var main = FindControl<RadialContainer>("Main");
|
||||
|
||||
var emotes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
|
||||
foreach (var emote in emotes)
|
||||
{
|
||||
var player = _playerManager.LocalSession?.AttachedEntity;
|
||||
if (emote.Category == EmoteCategory.Invalid ||
|
||||
emote.ChatTriggers.Count == 0 ||
|
||||
!(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) ||
|
||||
whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
|
||||
continue;
|
||||
|
||||
if (!emote.Available &&
|
||||
_entManager.TryGetComponent<SpeechComponent>(player.Value, out var speech) &&
|
||||
!speech.AllowedEmotes.Contains(emote.ID))
|
||||
continue;
|
||||
|
||||
var parent = FindControl<RadialContainer>(emote.Category.ToString());
|
||||
|
||||
var button = new EmoteMenuButton
|
||||
{
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = Loc.GetString(emote.Name),
|
||||
ProtoId = emote.ID,
|
||||
};
|
||||
|
||||
var tex = new TextureRect
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
Texture = spriteSystem.Frame0(emote.Icon),
|
||||
TextureScale = new Vector2(2f, 2f),
|
||||
};
|
||||
|
||||
button.AddChild(tex);
|
||||
parent.AddChild(button);
|
||||
foreach (var child in main.Children)
|
||||
{
|
||||
if (child is not RadialMenuTextureButton castChild)
|
||||
continue;
|
||||
|
||||
if (castChild.TargetLayer == emote.Category.ToString())
|
||||
{
|
||||
castChild.Visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set up menu actions
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child is not RadialContainer container)
|
||||
continue;
|
||||
AddEmoteClickAction(container);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddEmoteClickAction(RadialContainer container)
|
||||
{
|
||||
foreach (var child in container.Children)
|
||||
{
|
||||
if (child is not EmoteMenuButton castChild)
|
||||
continue;
|
||||
|
||||
castChild.OnButtonUp += _ =>
|
||||
{
|
||||
OnPlayEmote?.Invoke(castChild.ProtoId);
|
||||
Close();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed class EmoteMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<EmotePrototype> ProtoId { get; set; }
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
@@ -14,6 +13,7 @@ using System.Numerics;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Client.Graphics;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Chemistry.UI
|
||||
{
|
||||
@@ -24,6 +24,10 @@ namespace Content.Client.Chemistry.UI
|
||||
public sealed partial class ChemMasterWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public event Action<BaseButton.ButtonEventArgs, ReagentButton>? OnReagentButtonPressed;
|
||||
public readonly Button[] PillTypeButtons;
|
||||
|
||||
@@ -38,6 +42,8 @@ namespace Content.Client.Chemistry.UI
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entityManager.System<SpriteSystem>();
|
||||
|
||||
// Pill type selection buttons, in total there are 20 pills.
|
||||
// Pill rsi file should have states named as pill1, pill2, and so on.
|
||||
var resourcePath = new ResPath(PillsRsiPath);
|
||||
@@ -69,7 +75,7 @@ namespace Content.Client.Chemistry.UI
|
||||
var specifier = new SpriteSpecifier.Rsi(resourcePath, "pill" + (i + 1));
|
||||
TextureRect pillTypeTexture = new TextureRect
|
||||
{
|
||||
Texture = specifier.Frame0(),
|
||||
Texture = _sprite.Frame0(specifier),
|
||||
TextureScale = new Vector2(1.75f, 1.75f),
|
||||
Stretch = TextureRect.StretchMode.KeepCentered,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -37,7 +37,7 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
|
||||
if (TryComp(uid, out AnimationPlayerComponent? animPlayer)
|
||||
&& !AnimationSystem.HasRunningAnimation(uid, animPlayer, FoamVisualsComponent.AnimationKey))
|
||||
{
|
||||
AnimationSystem.Play(uid, animPlayer, comp.Animation, FoamVisualsComponent.AnimationKey);
|
||||
AnimationSystem.Play((uid, animPlayer), comp.Animation, FoamVisualsComponent.AnimationKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Vapor;
|
||||
using Content.Shared.Vapor;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class VaporVisualizerSystem : VisualizerSystem<VaporVisualsCompone
|
||||
TryComp<AnimationPlayerComponent>(uid, out var animPlayer) &&
|
||||
!AnimationSystem.HasRunningAnimation(uid, animPlayer, VaporVisualsComponent.AnimationKey))
|
||||
{
|
||||
AnimationSystem.Play(uid, animPlayer, comp.VaporFlick, VaporVisualsComponent.AnimationKey);
|
||||
AnimationSystem.Play((uid, animPlayer), comp.VaporFlick, VaporVisualsComponent.AnimationKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using Content.Client.ContextMenu.UI;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -27,14 +23,16 @@ public sealed class ExamineButton : ContainerButton
|
||||
public TextureRect Icon;
|
||||
|
||||
public ExamineVerb Verb;
|
||||
private SpriteSystem _sprite;
|
||||
|
||||
public ExamineButton(ExamineVerb verb)
|
||||
public ExamineButton(ExamineVerb verb, SpriteSystem spriteSystem)
|
||||
{
|
||||
Margin = new Thickness(Thickness, Thickness, Thickness, Thickness);
|
||||
|
||||
SetOnlyStyleClass(StyleClassExamineButton);
|
||||
|
||||
Verb = verb;
|
||||
_sprite = spriteSystem;
|
||||
|
||||
if (verb.Disabled)
|
||||
{
|
||||
@@ -61,7 +59,7 @@ public sealed class ExamineButton : ContainerButton
|
||||
|
||||
if (verb.Icon != null)
|
||||
{
|
||||
Icon.Texture = verb.Icon.Frame0();
|
||||
Icon.Texture = _sprite.Frame0(verb.Icon);
|
||||
Icon.Stretch = TextureRect.StretchMode.KeepAspectCentered;
|
||||
|
||||
AddChild(Icon);
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace Content.Client.Examine
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly VerbSystem _verbSystem = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public const string StyleClassEntityTooltip = "entity-tooltip";
|
||||
|
||||
@@ -332,7 +333,7 @@ namespace Content.Client.Examine
|
||||
if (!examine.ShowOnExamineTooltip)
|
||||
continue;
|
||||
|
||||
var button = new ExamineButton(examine);
|
||||
var button = new ExamineButton(examine, _sprite);
|
||||
|
||||
if (examine.HoverVerb)
|
||||
{
|
||||
|
||||
@@ -85,7 +85,7 @@ public sealed class RotatingLightSystem : SharedRotatingLightSystem
|
||||
|
||||
if (!_animations.HasRunningAnimation(uid, player, AnimKey))
|
||||
{
|
||||
_animations.Play(uid, player, GetAnimation(comp.Speed), AnimKey);
|
||||
_animations.Play((uid, player), GetAnimation(comp.Speed), AnimKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using Content.Shared.Light;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -53,13 +52,14 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
|
||||
/// </summary>
|
||||
private void OnAnimationCompleted(EntityUid uid, PoweredLightVisualsComponent comp, AnimationCompletedEvent args)
|
||||
{
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var animationPlayer))
|
||||
return;
|
||||
if (args.Key != PoweredLightVisualsComponent.BlinkingAnimationKey)
|
||||
return;
|
||||
|
||||
if(!comp.IsBlinking)
|
||||
return;
|
||||
|
||||
AnimationSystem.Play(uid, Comp<AnimationPlayerComponent>(uid), BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
|
||||
AnimationSystem.Play((uid, animationPlayer), BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,7 +76,7 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
|
||||
var animationPlayer = EnsureComp<AnimationPlayerComponent>(uid);
|
||||
if (shouldBeBlinking)
|
||||
{
|
||||
AnimationSystem.Play(uid, animationPlayer, BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
|
||||
AnimationSystem.Play((uid, animationPlayer), BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
|
||||
}
|
||||
else if (AnimationSystem.HasRunningAnimation(uid, animationPlayer, PoweredLightVisualsComponent.BlinkingAnimationKey))
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.ContextMenu.UI;
|
||||
@@ -149,7 +149,7 @@ public sealed class MappingState : GameplayStateBase
|
||||
{
|
||||
Deselect();
|
||||
|
||||
var coords = args.Coordinates.ToMap(_entityManager, _transform);
|
||||
var coords = _transform.ToMapCoordinates(args.Coordinates);
|
||||
if (_verbs.TryGetEntityMenuEntities(coords, out var entities))
|
||||
_entityMenuController.OpenRootMenu(entities);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Movement.Systems;
|
||||
|
||||
@@ -10,8 +9,6 @@ namespace Content.Client.Movement.Systems;
|
||||
/// </summary>
|
||||
public sealed class ClientSpriteMovementSystem : SharedSpriteMovementSystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private EntityQuery<SpriteComponent> _spriteQuery;
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Movement.Components;
|
||||
using Content.Shared.Camera;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Map;
|
||||
@@ -16,8 +14,6 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedContentEyeSystem _contentEye = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
// This value is here to make sure the user doesn't have to move their mouse
|
||||
@@ -42,7 +38,7 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
|
||||
|
||||
public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
|
||||
{
|
||||
var localPlayer = _player.LocalPlayer?.ControlledEntity;
|
||||
var localPlayer = _player.LocalEntity;
|
||||
var mousePos = _inputManager.MouseScreenPosition;
|
||||
var screenSize = _clyde.MainWindow.Size;
|
||||
var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;
|
||||
|
||||
@@ -49,13 +49,17 @@ public sealed class JetpackSystem : SharedJetpackSystem
|
||||
|
||||
// TODO: Please don't copy-paste this I beg
|
||||
// make a generic particle emitter system / actual particles instead.
|
||||
var query = EntityQueryEnumerator<ActiveJetpackComponent>();
|
||||
var query = EntityQueryEnumerator<ActiveJetpackComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
while (query.MoveNext(out var uid, out var comp, out var xform))
|
||||
{
|
||||
if (_timing.CurTime < comp.TargetTime)
|
||||
continue;
|
||||
if (_transform.InRange(xform.Coordinates, comp.LastCoordinates, comp.MaxDistance))
|
||||
{
|
||||
if (_timing.CurTime < comp.TargetTime)
|
||||
continue;
|
||||
}
|
||||
|
||||
comp.LastCoordinates = _transform.GetMoverCoordinates(xform.Coordinates);
|
||||
comp.TargetTime = _timing.CurTime + TimeSpan.FromSeconds(comp.EffectCooldown);
|
||||
|
||||
CreateParticles(uid);
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:rcd="clr-namespace:Content.Client.RCD"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Note: The min size of the window just determine how close to the edge of the screen the center of the radial menu can be placed -->
|
||||
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
|
||||
|
||||
<!-- Entry layer (shows main categories) -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- Walls and flooring -->
|
||||
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Windows and grilles -->
|
||||
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Airlocks -->
|
||||
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Computer and machine frames -->
|
||||
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Lighting -->
|
||||
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
@@ -1,172 +0,0 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.RCD;
|
||||
using Content.Shared.RCD.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.RCD;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RCDMenu : RadialMenu
|
||||
{
|
||||
[Dependency] private readonly EntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private SharedPopupSystem _popup;
|
||||
private SpriteSystem _sprites;
|
||||
|
||||
public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
|
||||
|
||||
private EntityUid _owner;
|
||||
|
||||
public RCDMenu()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_popup = _entManager.System<SharedPopupSystem>();
|
||||
_sprites = _entManager.System<SpriteSystem>();
|
||||
|
||||
OnChildAdded += AddRCDMenuButtonOnClickActions;
|
||||
}
|
||||
|
||||
public void SetEntity(EntityUid uid)
|
||||
{
|
||||
_owner = uid;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
// Find the main radial container
|
||||
var main = FindControl<RadialContainer>("Main");
|
||||
|
||||
// Populate secondary radial containers
|
||||
if (!_entManager.TryGetComponent<RCDComponent>(_owner, out var rcd))
|
||||
return;
|
||||
|
||||
foreach (var protoId in rcd.AvailablePrototypes)
|
||||
{
|
||||
if (!_protoManager.TryIndex(protoId, out var proto))
|
||||
continue;
|
||||
|
||||
if (proto.Mode == RcdMode.Invalid)
|
||||
continue;
|
||||
|
||||
var parent = FindControl<RadialContainer>(proto.Category);
|
||||
var tooltip = Loc.GetString(proto.SetName);
|
||||
|
||||
if ((proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject) &&
|
||||
proto.Prototype != null && _protoManager.TryIndex(proto.Prototype, out var entProto, logError: false))
|
||||
{
|
||||
tooltip = Loc.GetString(entProto.Name);
|
||||
}
|
||||
|
||||
tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1));
|
||||
|
||||
var button = new RCDMenuButton()
|
||||
{
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = tooltip,
|
||||
ProtoId = protoId,
|
||||
};
|
||||
|
||||
if (proto.Sprite != null)
|
||||
{
|
||||
var tex = new TextureRect()
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
Texture = _sprites.Frame0(proto.Sprite),
|
||||
TextureScale = new Vector2(2f, 2f),
|
||||
};
|
||||
|
||||
button.AddChild(tex);
|
||||
}
|
||||
|
||||
parent.AddChild(button);
|
||||
|
||||
// Ensure that the button that transitions the menu to the associated category layer
|
||||
// is visible in the main radial container (as these all start with Visible = false)
|
||||
foreach (var child in main.Children)
|
||||
{
|
||||
if (child is not RadialMenuTextureButton castChild)
|
||||
continue;
|
||||
|
||||
if (castChild.TargetLayer == proto.Category)
|
||||
{
|
||||
castChild.Visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set up menu actions
|
||||
foreach (var child in Children)
|
||||
{
|
||||
AddRCDMenuButtonOnClickActions(child);
|
||||
}
|
||||
}
|
||||
|
||||
private static string OopsConcat(string a, string b)
|
||||
{
|
||||
// This exists to prevent Roslyn being clever and compiling something that fails sandbox checks.
|
||||
return a + b;
|
||||
}
|
||||
|
||||
private void AddRCDMenuButtonOnClickActions(Control control)
|
||||
{
|
||||
var radialContainer = control as RadialContainer;
|
||||
|
||||
if (radialContainer == null)
|
||||
return;
|
||||
|
||||
foreach (var child in radialContainer.Children)
|
||||
{
|
||||
var castChild = child as RCDMenuButton;
|
||||
|
||||
if (castChild == null)
|
||||
continue;
|
||||
|
||||
castChild.OnButtonUp += _ =>
|
||||
{
|
||||
SendRCDSystemMessageAction?.Invoke(castChild.ProtoId);
|
||||
|
||||
if (_playerManager.LocalSession?.AttachedEntity != null &&
|
||||
_protoManager.TryIndex(castChild.ProtoId, out var proto))
|
||||
{
|
||||
var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName)));
|
||||
|
||||
if (proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject)
|
||||
{
|
||||
var name = Loc.GetString(proto.SetName);
|
||||
|
||||
if (proto.Prototype != null &&
|
||||
_protoManager.TryIndex(proto.Prototype, out var entProto, logError: false))
|
||||
name = entProto.Name;
|
||||
|
||||
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
|
||||
}
|
||||
|
||||
// Popup message
|
||||
_popup.PopupClient(msg, _owner, _playerManager.LocalSession.AttachedEntity);
|
||||
}
|
||||
|
||||
Close();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RCDMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<RCDPrototype> ProtoId { get; set; }
|
||||
}
|
||||
@@ -1,20 +1,32 @@
|
||||
using Content.Client.Popups;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.RCD;
|
||||
using Content.Shared.RCD.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.RCD;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class RCDMenuBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[Dependency] private readonly IClyde _displayManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
private static readonly Dictionary<string, (string Tooltip, SpriteSpecifier Sprite)> PrototypesGroupingInfo
|
||||
= new Dictionary<string, (string Tooltip, SpriteSpecifier Sprite)>
|
||||
{
|
||||
["WallsAndFlooring"] = ("rcd-component-walls-and-flooring", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/walls_and_flooring.png"))),
|
||||
["WindowsAndGrilles"] = ("rcd-component-windows-and-grilles", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/windows_and_grilles.png"))),
|
||||
["Airlocks"] = ("rcd-component-airlocks", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/airlocks.png"))),
|
||||
["Electrical"] = ("rcd-component-electrical", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/multicoil.png"))),
|
||||
["Lighting"] = ("rcd-component-lighting", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/lighting.png"))),
|
||||
};
|
||||
|
||||
private RCDMenu? _menu;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
|
||||
private SimpleRadialMenu? _menu;
|
||||
|
||||
public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
@@ -25,19 +37,107 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<RCDMenu>();
|
||||
_menu.SetEntity(Owner);
|
||||
_menu.SendRCDSystemMessageAction += SendRCDSystemMessage;
|
||||
if (!EntMan.TryGetComponent<RCDComponent>(Owner, out var rcd))
|
||||
return;
|
||||
|
||||
// Open the menu, centered on the mouse
|
||||
var vpSize = _displayManager.ScreenSize;
|
||||
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
|
||||
_menu = this.CreateWindow<SimpleRadialMenu>();
|
||||
_menu.Track(Owner);
|
||||
var models = ConvertToButtons(rcd.AvailablePrototypes);
|
||||
_menu.SetButtons(models);
|
||||
|
||||
_menu.OpenOverMouseScreenPosition();
|
||||
}
|
||||
|
||||
public void SendRCDSystemMessage(ProtoId<RCDPrototype> protoId)
|
||||
private IEnumerable<RadialMenuNestedLayerOption> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes)
|
||||
{
|
||||
Dictionary<string, List<RadialMenuActionOption>> buttonsByCategory = new();
|
||||
foreach (var protoId in prototypes)
|
||||
{
|
||||
var prototype = _prototypeManager.Index(protoId);
|
||||
if (!PrototypesGroupingInfo.TryGetValue(prototype.Category, out var groupInfo))
|
||||
continue;
|
||||
|
||||
if (!buttonsByCategory.TryGetValue(prototype.Category, out var list))
|
||||
{
|
||||
list = new List<RadialMenuActionOption>();
|
||||
buttonsByCategory.Add(prototype.Category, list);
|
||||
}
|
||||
|
||||
var actionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
|
||||
{
|
||||
Sprite = prototype.Sprite,
|
||||
ToolTip = GetTooltip(prototype)
|
||||
};
|
||||
list.Add(actionOption);
|
||||
}
|
||||
|
||||
var models = new RadialMenuNestedLayerOption[buttonsByCategory.Count];
|
||||
var i = 0;
|
||||
foreach (var (key, list) in buttonsByCategory)
|
||||
{
|
||||
var groupInfo = PrototypesGroupingInfo[key];
|
||||
models[i] = new RadialMenuNestedLayerOption(list)
|
||||
{
|
||||
Sprite = groupInfo.Sprite,
|
||||
ToolTip = Loc.GetString(groupInfo.Tooltip)
|
||||
};
|
||||
i++;
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
private void HandleMenuOptionClick(RCDPrototype proto)
|
||||
{
|
||||
// A predicted message cannot be used here as the RCD UI is closed immediately
|
||||
// after this message is sent, which will stop the server from receiving it
|
||||
SendMessage(new RCDSystemMessage(protoId));
|
||||
SendMessage(new RCDSystemMessage(proto.ID));
|
||||
|
||||
|
||||
if (_playerManager.LocalSession?.AttachedEntity == null)
|
||||
return;
|
||||
|
||||
var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName)));
|
||||
|
||||
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject)
|
||||
{
|
||||
var name = Loc.GetString(proto.SetName);
|
||||
|
||||
if (proto.Prototype != null &&
|
||||
_prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
|
||||
name = entProto.Name;
|
||||
|
||||
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
|
||||
}
|
||||
|
||||
// Popup message
|
||||
var popup = EntMan.System<PopupSystem>();
|
||||
popup.PopupClient(msg, Owner, _playerManager.LocalSession.AttachedEntity);
|
||||
}
|
||||
|
||||
private string GetTooltip(RCDPrototype proto)
|
||||
{
|
||||
string tooltip;
|
||||
|
||||
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject
|
||||
&& proto.Prototype != null
|
||||
&& _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
|
||||
{
|
||||
tooltip = Loc.GetString(entProto.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
tooltip = Loc.GetString(proto.SetName);
|
||||
}
|
||||
|
||||
tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1));
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private static string OopsConcat(string a, string b)
|
||||
{
|
||||
// This exists to prevent Roslyn being clever and compiling something that fails sandbox checks.
|
||||
return a + b;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed class RotationVisualizerSystem : SharedRotationVisualsSystem
|
||||
// Stop the current rotate animation and then start a new one
|
||||
if (_animation.HasRunningAnimation(animationComp, animationKey))
|
||||
{
|
||||
_animation.Stop(animationComp, animationKey);
|
||||
_animation.Stop((uid, animationComp), animationKey);
|
||||
}
|
||||
|
||||
var animation = new Animation
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Silicons.StationAi;
|
||||
|
||||
public sealed class StationAiBoundUserInterface : BoundUserInterface
|
||||
public sealed class StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
private StationAiMenu? _menu;
|
||||
|
||||
public StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
private SimpleRadialMenu? _menu;
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
_menu = this.CreateWindow<StationAiMenu>();
|
||||
_menu.Track(Owner);
|
||||
|
||||
_menu.OnAiRadial += args =>
|
||||
var ev = new GetStationAiRadialEvent();
|
||||
EntMan.EventBus.RaiseLocalEvent(Owner, ref ev);
|
||||
|
||||
_menu = this.CreateWindow<SimpleRadialMenu>();
|
||||
_menu.Track(Owner);
|
||||
var buttonModels = ConvertToButtons(ev.Actions);
|
||||
_menu.SetButtons(buttonModels);
|
||||
|
||||
_menu.Open();
|
||||
}
|
||||
|
||||
private IEnumerable<RadialMenuActionOption> ConvertToButtons(IReadOnlyList<StationAiRadial> actions)
|
||||
{
|
||||
var models = new RadialMenuActionOption[actions.Count];
|
||||
for (int i = 0; i < actions.Count; i++)
|
||||
{
|
||||
SendPredictedMessage(new StationAiRadialMessage()
|
||||
var action = actions[i];
|
||||
models[i] = new RadialMenuActionOption<BaseStationAiAction>(HandleRadialMenuClick, action.Event)
|
||||
{
|
||||
Event = args,
|
||||
});
|
||||
};
|
||||
Sprite = action.Sprite,
|
||||
ToolTip = action.Tooltip
|
||||
};
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
private void HandleRadialMenuClick(BaseStationAiAction p)
|
||||
{
|
||||
SendPredictedMessage(new StationAiRadialMessage { Event = p });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
</ui:RadialContainer>
|
||||
|
||||
</ui:RadialMenu>
|
||||
@@ -1,126 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Silicons.StationAi;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class StationAiMenu : RadialMenu
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public event Action<BaseStationAiAction>? OnAiRadial;
|
||||
|
||||
private EntityUid _tracked;
|
||||
|
||||
public StationAiMenu()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void Track(EntityUid owner)
|
||||
{
|
||||
_tracked = owner;
|
||||
|
||||
if (!_entManager.EntityExists(_tracked))
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
BuildButtons();
|
||||
UpdatePosition();
|
||||
}
|
||||
|
||||
private void BuildButtons()
|
||||
{
|
||||
var ev = new GetStationAiRadialEvent();
|
||||
_entManager.EventBus.RaiseLocalEvent(_tracked, ref ev);
|
||||
|
||||
var main = FindControl<RadialContainer>("Main");
|
||||
main.DisposeAllChildren();
|
||||
var sprites = _entManager.System<SpriteSystem>();
|
||||
|
||||
foreach (var action in ev.Actions)
|
||||
{
|
||||
// TODO: This radial boilerplate is quite annoying
|
||||
var button = new StationAiMenuButton(action.Event)
|
||||
{
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null,
|
||||
};
|
||||
|
||||
if (action.Sprite != null)
|
||||
{
|
||||
var texture = sprites.Frame0(action.Sprite);
|
||||
var scale = Vector2.One;
|
||||
|
||||
if (texture.Width <= 32)
|
||||
{
|
||||
scale *= 2;
|
||||
}
|
||||
|
||||
var tex = new TextureRect
|
||||
{
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
Texture = texture,
|
||||
TextureScale = scale,
|
||||
};
|
||||
|
||||
button.AddChild(tex);
|
||||
}
|
||||
|
||||
button.OnPressed += args =>
|
||||
{
|
||||
OnAiRadial?.Invoke(action.Event);
|
||||
Close();
|
||||
};
|
||||
main.AddChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdatePosition();
|
||||
}
|
||||
|
||||
private void UpdatePosition()
|
||||
{
|
||||
if (!_entManager.TryGetComponent(_tracked, out TransformComponent? xform))
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!xform.Coordinates.IsValid(_entManager))
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var coords = _entManager.System<SpriteSystem>().GetSpriteScreenCoordinates((_tracked, null, xform));
|
||||
|
||||
if (!coords.IsValid)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
OpenScreenAt(coords.Position, _clyde);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public BaseStationAiAction Action = action;
|
||||
}
|
||||
5
Content.Client/Temperature/Systems/EntityHeaterSystem.cs
Normal file
5
Content.Client/Temperature/Systems/EntityHeaterSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Temperature.Systems;
|
||||
|
||||
namespace Content.Client.Temperature.Systems;
|
||||
|
||||
public sealed partial class EntityHeaterSystem : SharedEntityHeaterSystem;
|
||||
121
Content.Client/Turrets/DeployableTurretSystem.cs
Normal file
121
Content.Client/Turrets/DeployableTurretSystem.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Content.Client.Power;
|
||||
using Content.Shared.Turrets;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Turrets;
|
||||
|
||||
public sealed partial class DeployableTurretSystem : SharedDeployableTurretSystem
|
||||
{
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DeployableTurretComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<DeployableTurretComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeLocalEvent<DeployableTurretComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||
}
|
||||
|
||||
private void OnComponentInit(Entity<DeployableTurretComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
ent.Comp.DeploymentAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(ent.Comp.DeploymentLength),
|
||||
AnimationTracks = {
|
||||
new AnimationTrackSpriteFlick() {
|
||||
LayerKey = DeployableTurretVisuals.Turret,
|
||||
KeyFrames = {new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.DeployingState, 0f)}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
ent.Comp.RetractionAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(ent.Comp.RetractionLength),
|
||||
AnimationTracks = {
|
||||
new AnimationTrackSpriteFlick() {
|
||||
LayerKey = DeployableTurretVisuals.Turret,
|
||||
KeyFrames = {new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.RetractingState, 0f)}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(Entity<DeployableTurretComponent> ent, ref AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != DeployableTurretComponent.AnimationKey)
|
||||
return;
|
||||
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
return;
|
||||
|
||||
if (!_appearance.TryGetData<DeployableTurretState>(ent, DeployableTurretVisuals.Turret, out var state))
|
||||
state = ent.Comp.VisualState;
|
||||
|
||||
// Convert to terminal state
|
||||
var targetState = state & DeployableTurretState.Deployed;
|
||||
|
||||
UpdateVisuals(ent, targetState, sprite, args.AnimationPlayer);
|
||||
}
|
||||
|
||||
private void OnAppearanceChange(Entity<DeployableTurretComponent> ent, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (!TryComp<AnimationPlayerComponent>(ent, out var animPlayer))
|
||||
return;
|
||||
|
||||
if (!_appearance.TryGetData<DeployableTurretState>(ent, DeployableTurretVisuals.Turret, out var state, args.Component))
|
||||
state = DeployableTurretState.Retracted;
|
||||
|
||||
UpdateVisuals(ent, state, args.Sprite, animPlayer);
|
||||
}
|
||||
|
||||
private void UpdateVisuals(Entity<DeployableTurretComponent> ent, DeployableTurretState state, SpriteComponent sprite, AnimationPlayerComponent? animPlayer = null)
|
||||
{
|
||||
if (!Resolve(ent, ref animPlayer))
|
||||
return;
|
||||
|
||||
if (_animation.HasRunningAnimation(ent, animPlayer, DeployableTurretComponent.AnimationKey))
|
||||
return;
|
||||
|
||||
if (state == ent.Comp.VisualState)
|
||||
return;
|
||||
|
||||
var targetState = state & DeployableTurretState.Deployed;
|
||||
var destinationState = ent.Comp.VisualState & DeployableTurretState.Deployed;
|
||||
|
||||
if (targetState != destinationState)
|
||||
targetState = targetState | DeployableTurretState.Retracting;
|
||||
|
||||
ent.Comp.VisualState = state;
|
||||
|
||||
// Toggle layer visibility
|
||||
sprite.LayerSetVisible(DeployableTurretVisuals.Weapon, (targetState & DeployableTurretState.Deployed) > 0);
|
||||
sprite.LayerSetVisible(PowerDeviceVisualLayers.Powered, HasAmmo(ent) && targetState == DeployableTurretState.Retracted);
|
||||
|
||||
// Change the visual state
|
||||
switch (targetState)
|
||||
{
|
||||
case DeployableTurretState.Deploying:
|
||||
_animation.Play((ent, animPlayer), (Animation)ent.Comp.DeploymentAnimation, DeployableTurretComponent.AnimationKey);
|
||||
break;
|
||||
|
||||
case DeployableTurretState.Retracting:
|
||||
_animation.Play((ent, animPlayer), (Animation)ent.Comp.RetractionAnimation, DeployableTurretComponent.AnimationKey);
|
||||
break;
|
||||
|
||||
case DeployableTurretState.Deployed:
|
||||
sprite.LayerSetState(DeployableTurretVisuals.Turret, ent.Comp.DeployedState);
|
||||
break;
|
||||
|
||||
case DeployableTurretState.Retracted:
|
||||
sprite.LayerSetState(DeployableTurretVisuals.Turret, ent.Comp.RetractedState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
@@ -143,11 +143,8 @@ public class RadialMenu : BaseWindow
|
||||
return children.First(x => x.Visible);
|
||||
}
|
||||
|
||||
public bool TryToMoveToNewLayer(string newLayer)
|
||||
public bool TryToMoveToNewLayer(Control newLayer)
|
||||
{
|
||||
if (newLayer == string.Empty)
|
||||
return false;
|
||||
|
||||
var currentLayer = GetCurrentActiveLayer();
|
||||
|
||||
if (currentLayer == null)
|
||||
@@ -161,7 +158,7 @@ public class RadialMenu : BaseWindow
|
||||
continue;
|
||||
|
||||
// Hide layers which are not of interest
|
||||
if (result == true || child.Name != newLayer)
|
||||
if (result == true || child != newLayer)
|
||||
{
|
||||
child.Visible = false;
|
||||
}
|
||||
@@ -186,6 +183,19 @@ public class RadialMenu : BaseWindow
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryToMoveToNewLayer(string targetLayerControlName)
|
||||
{
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child.Name == targetLayerControlName && child is RadialContainer)
|
||||
{
|
||||
return TryToMoveToNewLayer(child);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ReturnToPreviousLayer()
|
||||
{
|
||||
// Close the menu if the traversal path is empty
|
||||
@@ -296,9 +306,15 @@ public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
|
||||
public class RadialMenuTextureButton : RadialMenuTextureButtonBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will be moved to the named layer
|
||||
/// Upon clicking this button the radial menu will be moved to the layer of this control.
|
||||
/// </summary>
|
||||
public string TargetLayer { get; set; } = string.Empty;
|
||||
public Control? TargetLayer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Other way to set navigation to other container, as <see cref="TargetLayer"/>,
|
||||
/// but using <see cref="Control.Name"/> property of target <see cref="RadialContainer"/>.
|
||||
/// </summary>
|
||||
public string? TargetLayerControlName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A simple texture button that can move the user to a different layer within a radial menu
|
||||
@@ -311,7 +327,7 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
|
||||
|
||||
private void OnClicked(ButtonEventArgs args)
|
||||
{
|
||||
if (TargetLayer == string.Empty)
|
||||
if (TargetLayer == null && TargetLayerControlName == null)
|
||||
return;
|
||||
|
||||
var parent = FindParentMultiLayerContainer(this);
|
||||
@@ -319,7 +335,14 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
if (TargetLayer != null)
|
||||
{
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
parent.TryToMoveToNewLayer(TargetLayerControlName!);
|
||||
}
|
||||
}
|
||||
|
||||
private RadialMenu? FindParentMultiLayerContainer(Control control)
|
||||
@@ -387,7 +410,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
|
||||
private Color _hoverBorderColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render border of segment. Is false by default.
|
||||
/// Marker, that controls if border of segment should be rendered. Is false by default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default color of border is same as color of background. Use <see cref="BorderColor"/>
|
||||
@@ -400,13 +423,6 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
|
||||
/// </summary>
|
||||
public bool DrawBackground { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render separator lines.
|
||||
/// Separator lines are used to visually separate sector of radial menu items.
|
||||
/// Is true by default
|
||||
/// </summary>
|
||||
public bool DrawSeparators { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
@@ -520,7 +536,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
|
||||
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false);
|
||||
}
|
||||
|
||||
if (!_isWholeCircle && DrawSeparators)
|
||||
if (!_isWholeCircle && DrawBorder)
|
||||
{
|
||||
DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<ui:SimpleRadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
</ui:SimpleRadialMenu>
|
||||
279
Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
Normal file
279
Content.Client/UserInterface/Controls/SimpleRadialMenu.xaml.cs
Normal file
@@ -0,0 +1,279 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using System.Numerics;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public partial class SimpleRadialMenu : RadialMenu
|
||||
{
|
||||
private EntityUid? _attachMenuToEntity;
|
||||
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
|
||||
public SimpleRadialMenu()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void Track(EntityUid owner)
|
||||
{
|
||||
_attachMenuToEntity = owner;
|
||||
}
|
||||
|
||||
public void SetButtons(IEnumerable<RadialMenuOption> models, SimpleRadialMenuSettings? settings = null)
|
||||
{
|
||||
ClearExistingChildrenRadialButtons();
|
||||
|
||||
var sprites = _entManager.System<SpriteSystem>();
|
||||
Fill(models, sprites, Children, settings ?? new SimpleRadialMenuSettings());
|
||||
}
|
||||
|
||||
public void OpenOverMouseScreenPosition()
|
||||
{
|
||||
var vpSize = _clyde.ScreenSize;
|
||||
OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
|
||||
}
|
||||
|
||||
private void Fill(
|
||||
IEnumerable<RadialMenuOption> models,
|
||||
SpriteSystem sprites,
|
||||
ICollection<Control> rootControlChildren,
|
||||
SimpleRadialMenuSettings settings
|
||||
)
|
||||
{
|
||||
var rootContainer = new RadialContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
InitialRadius = settings.DefaultContainerRadius,
|
||||
ReserveSpaceForHiddenChildren = false,
|
||||
Visible = true
|
||||
};
|
||||
rootControlChildren.Add(rootContainer);
|
||||
|
||||
foreach (var model in models)
|
||||
{
|
||||
if (model is RadialMenuNestedLayerOption nestedMenuModel)
|
||||
{
|
||||
var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings);
|
||||
linkButton.Visible = true;
|
||||
rootContainer.AddChild(linkButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rootButtons = ConvertToButton(model, sprites, settings, false);
|
||||
rootContainer.AddChild(rootButtons);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RadialMenuTextureButton RecursiveContainerExtraction(
|
||||
SpriteSystem sprites,
|
||||
ICollection<Control> rootControlChildren,
|
||||
RadialMenuNestedLayerOption model,
|
||||
SimpleRadialMenuSettings settings
|
||||
)
|
||||
{
|
||||
var container = new RadialContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
InitialRadius = model.ContainerRadius!.Value,
|
||||
ReserveSpaceForHiddenChildren = false,
|
||||
Visible = false
|
||||
};
|
||||
foreach (var nested in model.Nested)
|
||||
{
|
||||
if (nested is RadialMenuNestedLayerOption nestedMenuModel)
|
||||
{
|
||||
var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings);
|
||||
container.AddChild(linkButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
var button = ConvertToButton(nested, sprites, settings, false);
|
||||
container.AddChild(button);
|
||||
}
|
||||
}
|
||||
rootControlChildren.Add(container);
|
||||
|
||||
var thisLayerLinkButton = ConvertToButton(model, sprites, settings, true);
|
||||
thisLayerLinkButton.TargetLayer = container;
|
||||
return thisLayerLinkButton;
|
||||
}
|
||||
|
||||
private RadialMenuTextureButton ConvertToButton(
|
||||
RadialMenuOption model,
|
||||
SpriteSystem sprites,
|
||||
SimpleRadialMenuSettings settings,
|
||||
bool haveNested
|
||||
)
|
||||
{
|
||||
var button = settings.UseSectors
|
||||
? ConvertToButtonWithSector(model, settings)
|
||||
: new RadialMenuTextureButton();
|
||||
button.SetSize = new Vector2(64f, 64f);
|
||||
button.ToolTip = model.ToolTip;
|
||||
if (model.Sprite != null)
|
||||
{
|
||||
var scale = Vector2.One;
|
||||
|
||||
var texture = sprites.Frame0(model.Sprite);
|
||||
if (texture.Width <= 32)
|
||||
{
|
||||
scale *= 2;
|
||||
}
|
||||
|
||||
button.TextureNormal = texture;
|
||||
button.Scale = scale;
|
||||
}
|
||||
|
||||
if (model is RadialMenuActionOption actionOption)
|
||||
{
|
||||
button.OnPressed += _ =>
|
||||
{
|
||||
actionOption.OnPressed?.Invoke();
|
||||
if(!haveNested)
|
||||
Close();
|
||||
};
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings)
|
||||
{
|
||||
var button = new RadialMenuTextureButtonWithSector
|
||||
{
|
||||
DrawBorder = settings.DisplayBorders,
|
||||
DrawBackground = !settings.NoBackground
|
||||
};
|
||||
if (model.BackgroundColor.HasValue)
|
||||
{
|
||||
button.BackgroundColor = model.BackgroundColor.Value;
|
||||
}
|
||||
|
||||
if (model.HoverBackgroundColor.HasValue)
|
||||
{
|
||||
button.HoverBackgroundColor = model.HoverBackgroundColor.Value;
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private void ClearExistingChildrenRadialButtons()
|
||||
{
|
||||
var toRemove = new List<Control>(ChildCount);
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child != ContextualButton && child != MenuOuterAreaButton)
|
||||
{
|
||||
toRemove.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var control in toRemove)
|
||||
{
|
||||
Children.Remove(control);
|
||||
}
|
||||
}
|
||||
|
||||
#region target entity tracking
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
if (_attachMenuToEntity != null)
|
||||
{
|
||||
UpdatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePosition()
|
||||
{
|
||||
if (!_entManager.TryGetComponent(_attachMenuToEntity, out TransformComponent? xform))
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!xform.Coordinates.IsValid(_entManager))
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var coords = _entManager.System<SpriteSystem>().GetSpriteScreenCoordinates((_attachMenuToEntity.Value, null, xform));
|
||||
|
||||
if (!coords.IsValid)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
OpenScreenAt(coords.Position, _clyde);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
|
||||
public abstract class RadialMenuOption
|
||||
{
|
||||
public string? ToolTip { get; init; }
|
||||
|
||||
public SpriteSpecifier? Sprite { get; init; }
|
||||
public Color? BackgroundColor { get; set; }
|
||||
public Color? HoverBackgroundColor { get; set; }
|
||||
}
|
||||
|
||||
public class RadialMenuActionOption(Action onPressed) : RadialMenuOption
|
||||
{
|
||||
public Action OnPressed { get; } = onPressed;
|
||||
}
|
||||
|
||||
public class RadialMenuActionOption<T>(Action<T> onPressed, T data)
|
||||
: RadialMenuActionOption(onPressed: () => onPressed(data));
|
||||
|
||||
public class RadialMenuNestedLayerOption(IReadOnlyCollection<RadialMenuOption> nested, float containerRadius = 100)
|
||||
: RadialMenuOption
|
||||
{
|
||||
public float? ContainerRadius { get; } = containerRadius;
|
||||
|
||||
public IReadOnlyCollection<RadialMenuOption> Nested { get; } = nested;
|
||||
}
|
||||
|
||||
public class SimpleRadialMenuSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Default container draw radius. Is going to be further affected by per sector increment.
|
||||
/// </summary>
|
||||
public int DefaultContainerRadius = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, if sector-buttons should be used.
|
||||
/// </summary>
|
||||
public bool UseSectors = true;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, if border of buttons should be rendered. Can only be used when <see cref="UseSectors"/> = true.
|
||||
/// </summary>
|
||||
public bool DisplayBorders = true;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, if sector background should not be rendered. Can only be used when <see cref="UseSectors"/> = true.
|
||||
/// </summary>
|
||||
public bool NoBackground = false;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Whitelist;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Emotes;
|
||||
|
||||
@@ -18,11 +19,19 @@ namespace Content.Client.UserInterface.Systems.Emotes;
|
||||
public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayState>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IClyde _displayManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.EmotesButton;
|
||||
private EmotesMenu? _menu;
|
||||
private SimpleRadialMenu? _menu;
|
||||
|
||||
private static readonly Dictionary<EmoteCategory, (string Tooltip, SpriteSpecifier Sprite)> EmoteGroupingInfo
|
||||
= new Dictionary<EmoteCategory, (string Tooltip, SpriteSpecifier Sprite)>
|
||||
{
|
||||
[EmoteCategory.General] = ("emote-menu-category-general", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"))),
|
||||
[EmoteCategory.Hands] = ("emote-menu-category-hands", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"))),
|
||||
[EmoteCategory.Vocal] = ("emote-menu-category-vocal", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Emotes/vocal.png"))),
|
||||
};
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
@@ -42,10 +51,16 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
|
||||
if (_menu == null)
|
||||
{
|
||||
// setup window
|
||||
_menu = UIManager.CreateWindow<EmotesMenu>();
|
||||
var prototypes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
|
||||
var models = ConvertToButtons(prototypes);
|
||||
|
||||
_menu = new SimpleRadialMenu();
|
||||
_menu.SetButtons(models);
|
||||
|
||||
_menu.Open();
|
||||
|
||||
_menu.OnClose += OnWindowClosed;
|
||||
_menu.OnOpen += OnWindowOpen;
|
||||
_menu.OnPlayEmote += OnPlayEmote;
|
||||
|
||||
if (EmotesButton != null)
|
||||
EmotesButton.SetClickPressed(true);
|
||||
@@ -56,16 +71,13 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
|
||||
}
|
||||
else
|
||||
{
|
||||
// Open the menu, centered on the mouse
|
||||
var vpSize = _displayManager.ScreenSize;
|
||||
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
|
||||
_menu.OpenOverMouseScreenPosition();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_menu.OnClose -= OnWindowClosed;
|
||||
_menu.OnOpen -= OnWindowOpen;
|
||||
_menu.OnPlayEmote -= OnPlayEmote;
|
||||
|
||||
if (EmotesButton != null)
|
||||
EmotesButton.SetClickPressed(false);
|
||||
@@ -118,8 +130,62 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
|
||||
_menu = null;
|
||||
}
|
||||
|
||||
private void OnPlayEmote(ProtoId<EmotePrototype> protoId)
|
||||
private IEnumerable<RadialMenuOption> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes)
|
||||
{
|
||||
_entityManager.RaisePredictiveEvent(new PlayEmoteMessage(protoId));
|
||||
var whitelistSystem = EntitySystemManager.GetEntitySystem<EntityWhitelistSystem>();
|
||||
var player = _playerManager.LocalSession?.AttachedEntity;
|
||||
|
||||
Dictionary<EmoteCategory, List<RadialMenuOption>> emotesByCategory = new();
|
||||
foreach (var emote in emotePrototypes)
|
||||
{
|
||||
if(emote.Category == EmoteCategory.Invalid)
|
||||
continue;
|
||||
|
||||
// only valid emotes that have ways to be triggered by chat and player have access / no restriction on
|
||||
if (emote.Category == EmoteCategory.Invalid
|
||||
|| emote.ChatTriggers.Count == 0
|
||||
|| !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value))
|
||||
|| whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
|
||||
continue;
|
||||
|
||||
if (!emote.Available
|
||||
&& EntityManager.TryGetComponent<SpeechComponent>(player.Value, out var speech)
|
||||
&& !speech.AllowedEmotes.Contains(emote.ID))
|
||||
continue;
|
||||
|
||||
if (!emotesByCategory.TryGetValue(emote.Category, out var list))
|
||||
{
|
||||
list = new List<RadialMenuOption>();
|
||||
emotesByCategory.Add(emote.Category, list);
|
||||
}
|
||||
|
||||
var actionOption = new RadialMenuActionOption<EmotePrototype>(HandleRadialButtonClick, emote)
|
||||
{
|
||||
Sprite = emote.Icon,
|
||||
ToolTip = Loc.GetString(emote.Name)
|
||||
};
|
||||
list.Add(actionOption);
|
||||
}
|
||||
|
||||
var models = new RadialMenuOption[emotesByCategory.Count];
|
||||
var i = 0;
|
||||
foreach (var (key, list) in emotesByCategory)
|
||||
{
|
||||
var tuple = EmoteGroupingInfo[key];
|
||||
|
||||
models[i] = new RadialMenuNestedLayerOption(list)
|
||||
{
|
||||
Sprite = tuple.Sprite,
|
||||
ToolTip = Loc.GetString(tuple.Tooltip)
|
||||
};
|
||||
i++;
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
private void HandleRadialButtonClick(EmotePrototype prototype)
|
||||
{
|
||||
_entityManager.RaisePredictiveEvent(new PlayEmoteMessage(prototype.ID));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ public sealed class StorageWindow : BaseWindow
|
||||
private ValueList<EntityUid> _contained = new();
|
||||
private ValueList<EntityUid> _toRemove = new();
|
||||
|
||||
// Manually store this because you can't have a 0x0 GridContainer but we still need to add child controls for 1x1 containers.
|
||||
private Vector2i _pieceGridSize;
|
||||
|
||||
private TextureButton? _backButton;
|
||||
|
||||
private bool _isDirty;
|
||||
@@ -408,11 +411,14 @@ public sealed class StorageWindow : BaseWindow
|
||||
_contained.Clear();
|
||||
_contained.AddRange(storageComp.Container.ContainedEntities.Reverse());
|
||||
|
||||
var width = boundingGrid.Width + 1;
|
||||
var height = boundingGrid.Height + 1;
|
||||
|
||||
// Build the grid representation
|
||||
if (_pieceGrid.Rows - 1 != boundingGrid.Height || _pieceGrid.Columns - 1 != boundingGrid.Width)
|
||||
if (_pieceGrid.Rows != _pieceGridSize.Y || _pieceGrid.Columns != _pieceGridSize.X)
|
||||
{
|
||||
_pieceGrid.Rows = boundingGrid.Height + 1;
|
||||
_pieceGrid.Columns = boundingGrid.Width + 1;
|
||||
_pieceGrid.Rows = height;
|
||||
_pieceGrid.Columns = width;
|
||||
_controlGrid.Clear();
|
||||
|
||||
for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++)
|
||||
@@ -430,6 +436,7 @@ public sealed class StorageWindow : BaseWindow
|
||||
}
|
||||
}
|
||||
|
||||
_pieceGridSize = new(width, height);
|
||||
_toRemove.Clear();
|
||||
|
||||
// Remove entities no longer relevant / Update existing ones
|
||||
|
||||
@@ -12,35 +12,6 @@ namespace Content.IntegrationTests.Tests.Access
|
||||
[TestOf(typeof(AccessReaderComponent))]
|
||||
public sealed class AccessReaderTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestProtoTags()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var accessName = server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(AccessReaderComponent));
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var ent in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!ent.Components.TryGetComponent(accessName, out var access))
|
||||
continue;
|
||||
|
||||
var reader = (AccessReaderComponent) access;
|
||||
var allTags = reader.AccessLists.SelectMany(c => c).Union(reader.DenyTags);
|
||||
|
||||
foreach (var level in allTags)
|
||||
{
|
||||
Assert.That(protoManager.HasIndex<AccessLevelPrototype>(level), $"Invalid access level: {level} found on {ent}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestTags()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Projectiles;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Embedding;
|
||||
@@ -88,4 +89,84 @@ public sealed class EmbedTest : InteractionTest
|
||||
AssertExists(projectile);
|
||||
await AssertEntityLookup(EmbeddableProtoId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws two embeddable projectiles at a target, then deletes them
|
||||
/// one at a time, making sure that they are tracked correctly and that
|
||||
/// the <see cref="EmbeddedContainerComponent"/> is removed once all
|
||||
/// projectiles are gone.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestDeleteWhileEmbedded()
|
||||
{
|
||||
// Spawn the target we're going to throw at
|
||||
await SpawnTarget(TargetProtoId);
|
||||
|
||||
// Give the player the embeddable to throw
|
||||
var projectile1 = await PlaceInHands(EmbeddableProtoId);
|
||||
Assert.That(TryComp<EmbeddableProjectileComponent>(projectile1, out var embedComp),
|
||||
$"{EmbeddableProtoId} does not have EmbeddableProjectileComponent.");
|
||||
// Make sure the projectile isn't already embedded into anything
|
||||
Assert.That(embedComp.EmbeddedIntoUid, Is.Null,
|
||||
$"Projectile already embedded into {SEntMan.ToPrettyString(embedComp.EmbeddedIntoUid)}.");
|
||||
|
||||
// Have the player throw the embeddable at the target
|
||||
await ThrowItem();
|
||||
|
||||
// Give the player a second embeddable to throw
|
||||
var projectile2 = await PlaceInHands(EmbeddableProtoId);
|
||||
Assert.That(TryComp<EmbeddableProjectileComponent>(projectile1, out var embedComp2),
|
||||
$"{EmbeddableProtoId} does not have EmbeddableProjectileComponent.");
|
||||
|
||||
// Wait a moment for the projectile to hit and embed
|
||||
await RunSeconds(0.5f);
|
||||
|
||||
// Make sure the projectile is embedded into the target
|
||||
Assert.That(embedComp.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)),
|
||||
"First projectile not embedded into target.");
|
||||
Assert.That(TryComp<EmbeddedContainerComponent>(out var containerComp),
|
||||
"Target was not given EmbeddedContainerComponent.");
|
||||
Assert.That(containerComp.EmbeddedObjects, Does.Contain(ToServer(projectile1)),
|
||||
"Target is not tracking the first projectile as embedded.");
|
||||
Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(1),
|
||||
"Target has unexpected EmbeddedObjects count.");
|
||||
|
||||
// Wait for the cooldown between throws
|
||||
await RunSeconds(Hands.ThrowCooldown.Seconds);
|
||||
|
||||
// Throw the second projectile
|
||||
await ThrowItem();
|
||||
|
||||
// Wait a moment for the second projectile to hit and embed
|
||||
await RunSeconds(0.5f);
|
||||
|
||||
Assert.That(embedComp2.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)),
|
||||
"Second projectile not embedded into target");
|
||||
AssertComp<EmbeddedContainerComponent>();
|
||||
Assert.That(containerComp.EmbeddedObjects, Does.Contain(ToServer(projectile1)),
|
||||
"Target is not tracking the second projectile as embedded.");
|
||||
Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(2),
|
||||
"Target EmbeddedObjects count did not increase with second projectile.");
|
||||
|
||||
// Delete the first projectile
|
||||
await Delete(projectile1);
|
||||
|
||||
Assert.That(containerComp.EmbeddedObjects, Does.Not.Contain(ToServer(projectile1)),
|
||||
"Target did not stop tracking first projectile after it was deleted.");
|
||||
Assert.That(containerComp.EmbeddedObjects, Does.Not.Contain(EntityUid.Invalid),
|
||||
"Target EmbeddedObjects contains an invalid entity.");
|
||||
foreach (var embedded in containerComp.EmbeddedObjects)
|
||||
{
|
||||
Assert.That(!SEntMan.Deleted(embedded),
|
||||
"Target EmbeddedObjects contains a deleted entity.");
|
||||
}
|
||||
Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(1),
|
||||
"Target EmbeddedObjects count did not decrease after deleting first projectile.");
|
||||
|
||||
// Delete the second projectile
|
||||
await Delete(projectile2);
|
||||
|
||||
Assert.That(!SEntMan.HasComponent<EmbeddedContainerComponent>(ToServer(Target)),
|
||||
"Target did not remove EmbeddedContainerComponent after both projectiles were deleted.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Client.LateJoin;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.ContentPack;
|
||||
@@ -14,7 +13,6 @@ public sealed class UiControlTest
|
||||
// You should not be adding to this.
|
||||
private Type[] _ignored = new Type[]
|
||||
{
|
||||
typeof(EmotesMenu),
|
||||
typeof(LateJoinGui),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Content.Server.Administration.Commands;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Zombies;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -17,6 +19,7 @@ public sealed partial class AdminVerbSystem
|
||||
{
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly ZombieSystem _zombie = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
private const string DefaultTraitorRule = "Traitor";
|
||||
@@ -36,6 +39,8 @@ public sealed partial class AdminVerbSystem
|
||||
[ValidatePrototypeId<StartingGearPrototype>]
|
||||
private const string PirateGearId = "PirateGear";
|
||||
|
||||
private readonly EntProtoId _paradoxCloneRuleId = "ParadoxCloneSpawn";
|
||||
|
||||
// All antag verbs have names so invokeverb works.
|
||||
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
|
||||
{
|
||||
@@ -157,5 +162,29 @@ public sealed partial class AdminVerbSystem
|
||||
Message = string.Join(": ", thiefName, Loc.GetString("admin-verb-make-thief")),
|
||||
};
|
||||
args.Verbs.Add(thief);
|
||||
|
||||
var paradoxCloneName = Loc.GetString("admin-verb-text-make-paradox-clone");
|
||||
Verb paradox = new()
|
||||
{
|
||||
Text = paradoxCloneName,
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "ParadoxClone"),
|
||||
Act = () =>
|
||||
{
|
||||
var ruleEnt = _gameTicker.AddGameRule(_paradoxCloneRuleId);
|
||||
|
||||
if (!TryComp<ParadoxCloneRuleComponent>(ruleEnt, out var paradoxCloneRuleComp))
|
||||
return;
|
||||
|
||||
paradoxCloneRuleComp.OriginalBody = args.Target; // override the target player
|
||||
|
||||
_gameTicker.StartGameRule(ruleEnt);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = string.Join(": ", paradoxCloneName, Loc.GetString("admin-verb-make-paradox-clone")),
|
||||
};
|
||||
|
||||
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
|
||||
args.Verbs.Add(paradox);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,14 +81,14 @@ public sealed class ProjectileAnomalySystem : EntitySystem
|
||||
EntityCoordinates targetCoords,
|
||||
float severity)
|
||||
{
|
||||
var mapPos = coords.ToMap(EntityManager, _xform);
|
||||
var mapPos = _xform.ToMapCoordinates(coords);
|
||||
|
||||
var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out _)
|
||||
? coords.WithEntityId(gridUid, EntityManager)
|
||||
? _xform.WithEntityId(coords, gridUid)
|
||||
: new(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
|
||||
|
||||
var ent = Spawn(component.ProjectilePrototype, spawnCoords);
|
||||
var direction = targetCoords.ToMapPos(EntityManager, _xform) - mapPos.Position;
|
||||
var direction = _xform.ToMapCoordinates(targetCoords).Position - mapPos.Position;
|
||||
|
||||
if (!TryComp<ProjectileComponent>(ent, out var comp))
|
||||
return;
|
||||
|
||||
@@ -16,7 +16,6 @@ public sealed class TechAnomalySystem : EntitySystem
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly BeamSystem _beam = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
@@ -17,6 +18,7 @@ public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
|
||||
[Dependency] private readonly PowerReceiverSystem _power = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -25,33 +27,33 @@ public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceUpdateEvent>(OnPumpUpdated);
|
||||
}
|
||||
|
||||
private void OnPumpUpdated(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceUpdateEvent args)
|
||||
private void OnPumpUpdated(Entity<GasPressurePumpComponent> ent, ref AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
if (!pump.Enabled
|
||||
|| (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|
||||
|| !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
|
||||
if (!ent.Comp.Enabled
|
||||
|| !_power.IsPowered(ent)
|
||||
|| !_nodeContainer.TryGetNodes(ent.Owner, ent.Comp.InletName, ent.Comp.OutletName, out PipeNode? inlet, out PipeNode? outlet))
|
||||
{
|
||||
_ambientSoundSystem.SetAmbience(uid, false);
|
||||
_ambientSoundSystem.SetAmbience(ent, false);
|
||||
return;
|
||||
}
|
||||
|
||||
var outputStartingPressure = outlet.Air.Pressure;
|
||||
|
||||
if (outputStartingPressure >= pump.TargetPressure)
|
||||
if (outputStartingPressure >= ent.Comp.TargetPressure)
|
||||
{
|
||||
_ambientSoundSystem.SetAmbience(uid, false);
|
||||
_ambientSoundSystem.SetAmbience(ent, false);
|
||||
return; // No need to pump gas if target has been reached.
|
||||
}
|
||||
|
||||
if (inlet.Air.TotalMoles > 0 && inlet.Air.Temperature > 0)
|
||||
{
|
||||
// We calculate the necessary moles to transfer using our good ol' friend PV=nRT.
|
||||
var pressureDelta = pump.TargetPressure - outputStartingPressure;
|
||||
var pressureDelta = ent.Comp.TargetPressure - outputStartingPressure;
|
||||
var transferMoles = (pressureDelta * outlet.Air.Volume) / (inlet.Air.Temperature * Atmospherics.R);
|
||||
|
||||
var removed = inlet.Air.Remove(transferMoles);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f);
|
||||
_ambientSoundSystem.SetAmbience(ent, removed.TotalMoles > 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Atmos.Piping.Binary.Components;
|
||||
using Content.Server.Atmos.Piping.Unary.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.EntitySystems;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.Construction.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
@@ -16,7 +14,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
public sealed class GasPortableSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -246,7 +246,7 @@ internal sealed partial class ChatManager : IChatManager
|
||||
|
||||
Color? colorOverride = null;
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
|
||||
if (_adminManager.HasAdminFlag(player, AdminFlags.NameColor))
|
||||
{
|
||||
var prefs = _preferencesManager.GetPreferences(player.UserId);
|
||||
colorOverride = prefs.AdminOOCColor;
|
||||
|
||||
@@ -9,8 +9,17 @@ namespace Content.Server.Destructible
|
||||
[RegisterComponent]
|
||||
public sealed partial class DestructibleComponent : Component
|
||||
{
|
||||
[DataField("thresholds")]
|
||||
/// <summary>
|
||||
/// A list of damage thresholds for the entity;
|
||||
/// includes their triggers and resultant behaviors
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<DamageThreshold> Thresholds = new();
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether the entity has passed a damage threshold that causes it to break
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool IsBroken = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ namespace Content.Server.Destructible
|
||||
/// </summary>
|
||||
public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
|
||||
{
|
||||
component.IsBroken = false;
|
||||
|
||||
foreach (var threshold in component.Thresholds)
|
||||
{
|
||||
if (threshold.Reached(args.Damageable, this))
|
||||
@@ -96,6 +98,12 @@ namespace Content.Server.Destructible
|
||||
threshold.Execute(uid, this, EntityManager, args.Origin);
|
||||
}
|
||||
|
||||
if (threshold.OldTriggered)
|
||||
{
|
||||
component.IsBroken |= threshold.Behaviors.Any(b => b is DoActsBehavior doActsBehavior &&
|
||||
(doActsBehavior.HasAct(ThresholdActs.Breakage) || doActsBehavior.HasAct(ThresholdActs.Destruction)));
|
||||
}
|
||||
|
||||
// if destruction behavior (or some other deletion effect) occurred, don't run other triggers.
|
||||
if (EntityManager.IsQueuedForDeletion(uid) || Deleted(uid))
|
||||
return;
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Content.Server.GameTicking
|
||||
if (args.NewStatus != SessionStatus.Disconnected)
|
||||
{
|
||||
mind.Session = session;
|
||||
_pvsOverride.AddSessionOverride(GetNetEntity(mindId.Value), session);
|
||||
_pvsOverride.AddSessionOverride(mindId.Value, session);
|
||||
}
|
||||
|
||||
DebugTools.Assert(mind.Session == session);
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
if (ev.GameMap.IsGrid)
|
||||
{
|
||||
var mapUid = _map.CreateMap(out mapId);
|
||||
var mapUid = _map.CreateMap(out mapId, runMapInit: options?.InitializeMaps ?? false);
|
||||
if (!_loader.TryLoadGrid(mapId,
|
||||
ev.GameMap.MapPath,
|
||||
out var grid,
|
||||
@@ -557,7 +557,7 @@ namespace Content.Server.GameTicking
|
||||
|
||||
if (TryGetEntity(mind.OriginalOwnedEntity, out var entity) && pvsOverride)
|
||||
{
|
||||
_pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
|
||||
_pvsOverride.AddGlobalOverride(entity.Value);
|
||||
}
|
||||
|
||||
var roles = _roles.MindGetAllRoleInfo(mindId);
|
||||
|
||||
@@ -427,7 +427,7 @@ namespace Content.Server.GameTicking
|
||||
// Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
|
||||
// update which means we need to add a hack somewhere around it.
|
||||
var spawn = _robustRandom.Pick(_possiblePositions);
|
||||
var toMap = spawn.ToMap(EntityManager, _transform);
|
||||
var toMap = _transform.ToMapCoordinates(spawn);
|
||||
|
||||
if (_mapManager.TryFindGridAt(toMap, out var gridUid, out _))
|
||||
{
|
||||
|
||||
@@ -22,12 +22,19 @@ public sealed partial class ParadoxCloneRuleComponent : Component
|
||||
[DataField]
|
||||
public EntProtoId GibProto = "MobParadoxTimed";
|
||||
|
||||
/// <summary>
|
||||
/// Entity of the original player.
|
||||
/// Gets randomly chosen from all alive players if not specified.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? OriginalBody;
|
||||
|
||||
/// <summary>
|
||||
/// Mind entity of the original player.
|
||||
/// Gets assigned when cloning.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? Original;
|
||||
public EntityUid? OriginalMind;
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist for Objectives to be copied to the clone.
|
||||
|
||||
@@ -47,28 +47,42 @@ public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComp
|
||||
if (args.Session?.AttachedEntity is not { } spawner)
|
||||
return;
|
||||
|
||||
// get possible targets
|
||||
var allHumans = _mind.GetAliveHumans();
|
||||
|
||||
// we already checked when starting the gamerule, but someone might have died since then.
|
||||
if (allHumans.Count == 0)
|
||||
if (ent.Comp.OriginalBody != null) // target was overridden, for example by admin antag control
|
||||
{
|
||||
Log.Warning("Could not find any alive players to create a paradox clone from!");
|
||||
return;
|
||||
if (Deleted(ent.Comp.OriginalBody.Value) || !_mind.TryGetMind(ent.Comp.OriginalBody.Value, out var originalMindId, out var _))
|
||||
{
|
||||
Log.Warning("Could not find mind of target player to paradox clone!");
|
||||
return;
|
||||
}
|
||||
ent.Comp.OriginalMind = originalMindId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// get possible targets
|
||||
var allAliveHumanoids = _mind.GetAliveHumans();
|
||||
|
||||
// we already checked when starting the gamerule, but someone might have died since then.
|
||||
if (allAliveHumanoids.Count == 0)
|
||||
{
|
||||
Log.Warning("Could not find any alive players to create a paradox clone from!");
|
||||
return;
|
||||
}
|
||||
|
||||
// pick a random player
|
||||
var randomHumanoidMind = _random.Pick(allAliveHumanoids);
|
||||
ent.Comp.OriginalMind = randomHumanoidMind;
|
||||
ent.Comp.OriginalBody = randomHumanoidMind.Comp.OwnedEntity;
|
||||
|
||||
}
|
||||
|
||||
// pick a random player
|
||||
var playerToClone = _random.Pick(allHumans);
|
||||
var bodyToClone = playerToClone.Comp.OwnedEntity;
|
||||
|
||||
if (bodyToClone == null || !_cloning.TryCloning(bodyToClone.Value, _transform.GetMapCoordinates(spawner), ent.Comp.Settings, out var clone))
|
||||
if (ent.Comp.OriginalBody == null || !_cloning.TryCloning(ent.Comp.OriginalBody.Value, _transform.GetMapCoordinates(spawner), ent.Comp.Settings, out var clone))
|
||||
{
|
||||
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(bodyToClone)}");
|
||||
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(ent.Comp.OriginalBody)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var targetComp = EnsureComp<TargetOverrideComponent>(clone.Value);
|
||||
targetComp.Target = playerToClone.Owner; // set the kill target
|
||||
targetComp.Target = ent.Comp.OriginalMind; // set the kill target
|
||||
|
||||
var gibComp = EnsureComp<GibOnRoundEndComponent>(clone.Value);
|
||||
gibComp.SpawnProto = ent.Comp.GibProto;
|
||||
@@ -78,17 +92,16 @@ public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComp
|
||||
_sensor.SetAllSensors(clone.Value, SuitSensorMode.SensorOff);
|
||||
|
||||
args.Entity = clone;
|
||||
ent.Comp.Original = playerToClone.Owner;
|
||||
}
|
||||
|
||||
private void AfterAntagEntitySelected(Entity<ParadoxCloneRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
if (ent.Comp.Original == null)
|
||||
if (ent.Comp.OriginalMind == null)
|
||||
return;
|
||||
|
||||
if (!_mind.TryGetMind(args.EntityUid, out var cloneMindId, out var cloneMindComp))
|
||||
return;
|
||||
|
||||
_mind.CopyObjectives(ent.Comp.Original.Value, (cloneMindId, cloneMindComp), ent.Comp.ObjectiveWhitelist, ent.Comp.ObjectiveBlacklist);
|
||||
_mind.CopyObjectives(ent.Comp.OriginalMind.Value, (cloneMindId, cloneMindComp), ent.Comp.ObjectiveWhitelist, ent.Comp.ObjectiveBlacklist);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Shared.Examine;
|
||||
using Content.Shared.Guardian;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mobs;
|
||||
@@ -188,7 +189,9 @@ namespace Content.Server.Guardian
|
||||
// Can only inject things with the component...
|
||||
if (!HasComp<CanHostGuardianComponent>(target))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("guardian-activator-invalid-target"), user, user);
|
||||
var msg = Loc.GetString("guardian-activator-invalid-target", ("entity", Identity.Entity(target, EntityManager, user)));
|
||||
|
||||
_popupSystem.PopupEntity(msg, user, user);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,15 @@ namespace Content.Server.Hands.Systems
|
||||
[Dependency] private readonly PullingSystem _pullingSystem = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
|
||||
/// <summary>
|
||||
/// Items dropped when the holder falls down will be launched in
|
||||
/// a direction offset by up to this many degrees from the holder's
|
||||
/// movement direction.
|
||||
/// </summary>
|
||||
private const float DropHeldItemsSpread = 45;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -60,6 +69,8 @@ namespace Content.Server.Hands.Systems
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem))
|
||||
.Register<HandsSystem>();
|
||||
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -234,13 +245,13 @@ namespace Content.Server.Hands.Systems
|
||||
|
||||
private void OnDropHandItems(Entity<HandsComponent> entity, ref DropHandItemsEvent args)
|
||||
{
|
||||
var direction = EntityManager.TryGetComponent(entity, out PhysicsComponent? comp) ? comp.LinearVelocity / 50 : Vector2.Zero;
|
||||
var dropAngle = _random.NextFloat(0.8f, 1.2f);
|
||||
// If the holder doesn't have a physics component, they ain't moving
|
||||
var holderVelocity = _physicsQuery.TryComp(entity, out var physics) ? physics.LinearVelocity : Vector2.Zero;
|
||||
var spreadMaxAngle = Angle.FromDegrees(DropHeldItemsSpread);
|
||||
|
||||
var fellEvent = new FellDownEvent(entity);
|
||||
RaiseLocalEvent(entity, fellEvent, false);
|
||||
|
||||
var worldRotation = TransformSystem.GetWorldRotation(entity).ToVec();
|
||||
foreach (var hand in entity.Comp.Hands.Values)
|
||||
{
|
||||
if (hand.HeldEntity is not EntityUid held)
|
||||
@@ -255,10 +266,26 @@ namespace Content.Server.Hands.Systems
|
||||
if (!TryDrop(entity, hand, null, checkActionBlocker: false, handsComp: entity.Comp))
|
||||
continue;
|
||||
|
||||
// Rotate the item's throw vector a bit for each item
|
||||
var angleOffset = _random.NextAngle(-spreadMaxAngle, spreadMaxAngle);
|
||||
// Rotate the holder's velocity vector by the angle offset to get the item's velocity vector
|
||||
var itemVelocity = angleOffset.RotateVec(holderVelocity);
|
||||
// Decrease the distance of the throw by a random amount
|
||||
itemVelocity *= _random.NextFloat(1f);
|
||||
// Heavier objects don't get thrown as far
|
||||
// If the item doesn't have a physics component, it isn't going to get thrown anyway, but we'll assume infinite mass
|
||||
itemVelocity *= _physicsQuery.TryComp(held, out var heldPhysics) ? heldPhysics.InvMass : 0;
|
||||
// Throw at half the holder's intentional throw speed and
|
||||
// vary the speed a little to make it look more interesting
|
||||
var throwSpeed = entity.Comp.BaseThrowspeed * _random.NextFloat(0.45f, 0.55f);
|
||||
|
||||
_throwingSystem.TryThrow(held,
|
||||
_random.NextAngle().RotateVec(direction / dropAngle + worldRotation / 50),
|
||||
0.5f * dropAngle * _random.NextFloat(-0.9f, 1.1f),
|
||||
entity, 0);
|
||||
itemVelocity,
|
||||
throwSpeed,
|
||||
entity,
|
||||
pushbackRatio: 0,
|
||||
compensateFriction: false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
@@ -19,7 +17,6 @@ namespace Content.Server.Mapping
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public string Command => "mapping";
|
||||
public string Description => Loc.GetString("cmd-mapping-desc");
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Mapping;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -19,7 +16,6 @@ public sealed class MappingManager : IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IAdminManager _admin = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IServerNetManager _net = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Medical.Stethoscope.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an innate verb when equipped to use a stethoscope.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class StethoscopeComponent : Component
|
||||
{
|
||||
public bool IsActive = false;
|
||||
|
||||
[DataField("delay")]
|
||||
public float Delay = 2.5f;
|
||||
|
||||
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string Action = "ActionStethoscope";
|
||||
|
||||
[DataField("actionEntity")] public EntityUid? ActionEntity;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace Content.Server.Medical.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to let doctors use the stethoscope on people.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class WearingStethoscopeComponent : Component
|
||||
{
|
||||
public CancellationTokenSource? CancelToken;
|
||||
|
||||
[DataField("delay")]
|
||||
public float Delay = 2.5f;
|
||||
|
||||
public EntityUid Stethoscope = default!;
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Medical.Components;
|
||||
using Content.Server.Medical.Stethoscope.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Medical;
|
||||
using Content.Shared.Medical.Stethoscope;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Medical.Stethoscope
|
||||
{
|
||||
public sealed class StethoscopeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<StethoscopeComponent, ClothingGotEquippedEvent>(OnEquipped);
|
||||
SubscribeLocalEvent<StethoscopeComponent, ClothingGotUnequippedEvent>(OnUnequipped);
|
||||
SubscribeLocalEvent<WearingStethoscopeComponent, GetVerbsEvent<InnateVerb>>(AddStethoscopeVerb);
|
||||
SubscribeLocalEvent<StethoscopeComponent, GetItemActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<StethoscopeComponent, StethoscopeActionEvent>(OnStethoscopeAction);
|
||||
SubscribeLocalEvent<StethoscopeComponent, StethoscopeDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the component the verb event subs to if the equippee is wearing the stethoscope.
|
||||
/// </summary>
|
||||
private void OnEquipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
component.IsActive = true;
|
||||
|
||||
var wearingComp = EnsureComp<WearingStethoscopeComponent>(args.Wearer);
|
||||
wearingComp.Stethoscope = uid;
|
||||
}
|
||||
|
||||
private void OnUnequipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
if (!component.IsActive)
|
||||
return;
|
||||
|
||||
RemComp<WearingStethoscopeComponent>(args.Wearer);
|
||||
component.IsActive = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is raised when someone with WearingStethoscopeComponent requests verbs on an item.
|
||||
/// It returns if the target is not a mob.
|
||||
/// </summary>
|
||||
private void AddStethoscopeVerb(EntityUid uid, WearingStethoscopeComponent component, GetVerbsEvent<InnateVerb> args)
|
||||
{
|
||||
if (!args.CanInteract || !args.CanAccess)
|
||||
return;
|
||||
|
||||
if (!HasComp<MobStateComponent>(args.Target))
|
||||
return;
|
||||
|
||||
if (component.CancelToken != null)
|
||||
return;
|
||||
|
||||
if (!TryComp<StethoscopeComponent>(component.Stethoscope, out var stetho))
|
||||
return;
|
||||
|
||||
InnateVerb verb = new()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
StartListening(component.Stethoscope, uid, args.Target, stetho); // start doafter
|
||||
},
|
||||
Text = Loc.GetString("stethoscope-verb"),
|
||||
Icon = new SpriteSpecifier.Rsi(new ("Clothing/Neck/Misc/stethoscope.rsi"), "icon"),
|
||||
Priority = 2
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
|
||||
private void OnStethoscopeAction(EntityUid uid, StethoscopeComponent component, StethoscopeActionEvent args)
|
||||
{
|
||||
StartListening(uid, args.Performer, args.Target, component);
|
||||
}
|
||||
|
||||
private void OnGetActions(EntityUid uid, StethoscopeComponent component, GetItemActionsEvent args)
|
||||
{
|
||||
args.AddAction(ref component.ActionEntity, component.Action);
|
||||
}
|
||||
|
||||
// construct the doafter and start it
|
||||
private void StartListening(EntityUid scope, EntityUid user, EntityUid target, StethoscopeComponent comp)
|
||||
{
|
||||
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.Delay, new StethoscopeDoAfterEvent(), scope, target: target, used: scope)
|
||||
{
|
||||
NeedHand = true,
|
||||
BreakOnMove = true,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, StethoscopeComponent component, DoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Args.Target == null)
|
||||
return;
|
||||
|
||||
ExamineWithStethoscope(args.Args.User, args.Args.Target.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a value based on the total oxyloss of the target.
|
||||
/// Could be expanded in the future with reagent effects etc.
|
||||
/// The loc lines are taken from the goon wiki.
|
||||
/// </summary>
|
||||
public void ExamineWithStethoscope(EntityUid user, EntityUid target)
|
||||
{
|
||||
// The mob check seems a bit redundant but (1) they could conceivably have lost it since when the doafter started and (2) I need it for .IsDead()
|
||||
if (!HasComp<RespiratorComponent>(target) || !TryComp<MobStateComponent>(target, out var mobState) || _mobStateSystem.IsDead(target, mobState))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("stethoscope-dead"), target, user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<DamageableComponent>(target, out var damage))
|
||||
return;
|
||||
// these should probably get loc'd at some point before a non-english fork accidentally breaks a bunch of stuff that does this
|
||||
if (!damage.Damage.DamageDict.TryGetValue("Asphyxiation", out var value))
|
||||
return;
|
||||
|
||||
var message = GetDamageMessage(value);
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString(message), target, user);
|
||||
}
|
||||
|
||||
private string GetDamageMessage(FixedPoint2 totalOxyloss)
|
||||
{
|
||||
var msg = (int) totalOxyloss switch
|
||||
{
|
||||
< 20 => "stethoscope-normal",
|
||||
< 60 => "stethoscope-hyper",
|
||||
< 80 => "stethoscope-irregular",
|
||||
_ => "stethoscope-fucked"
|
||||
};
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,11 +85,11 @@ public sealed class MindSystem : SharedMindSystem
|
||||
{
|
||||
if (base.TryGetMind(user, out mindId, out mind))
|
||||
{
|
||||
DebugTools.Assert(_players.GetPlayerData(user).ContentData() is not { } data || data.Mind == mindId);
|
||||
DebugTools.Assert(!_players.TryGetPlayerData(user, out var playerData) || playerData.ContentData() is not { } data || data.Mind == mindId);
|
||||
return true;
|
||||
}
|
||||
|
||||
DebugTools.Assert(_players.GetPlayerData(user).ContentData()?.Mind == null);
|
||||
DebugTools.Assert(!_players.TryGetPlayerData(user, out var pData) || pData.ContentData()?.Mind == null);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,14 +28,15 @@ public sealed class RotateEyesCommand : IConsoleCommand
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
|
||||
foreach (var mover in entManager.EntityQuery<InputMoverComponent>(true))
|
||||
var query = entManager.EntityQueryEnumerator<InputMoverComponent>();
|
||||
while (query.MoveNext(out var uid, out var mover))
|
||||
{
|
||||
if (mover.TargetRelativeRotation.Equals(rotation))
|
||||
continue;
|
||||
|
||||
mover.TargetRelativeRotation = rotation;
|
||||
entManager.Dirty(mover);
|
||||
|
||||
entManager.Dirty(uid, mover);
|
||||
count++;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,6 @@ public sealed class BoundarySystem : EntitySystem
|
||||
// If for whatever reason you want to yeet them to the other side.
|
||||
// offset = new Angle(MathF.PI).RotateVec(offset);
|
||||
|
||||
_xform.SetWorldPosition(otherXform, center + offset);
|
||||
_xform.SetWorldPosition((args.OtherEntity, otherXform), center + offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Numerics;
|
||||
using Content.Server.Movement.Components;
|
||||
using Content.Server.Physics.Controllers;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Conveyor;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
@@ -122,6 +123,12 @@ public sealed class PullController : VirtualController
|
||||
|
||||
var pulled = pullerComp.Pulling;
|
||||
|
||||
// See update statement; this thing overwrites so many systems, DOESN'T EVEN LERP PROPERLY.
|
||||
// We had a throwing version but it occasionally had issues.
|
||||
// We really need the throwing version back.
|
||||
if (TryComp(pulled, out ConveyedComponent? conveyed) && conveyed.Conveying)
|
||||
return false;
|
||||
|
||||
if (!_pullableQuery.TryComp(pulled, out var pullable))
|
||||
return false;
|
||||
|
||||
@@ -132,9 +139,9 @@ public sealed class PullController : VirtualController
|
||||
|
||||
// Cap the distance
|
||||
var range = 2f;
|
||||
var fromUserCoords = coords.WithEntityId(player, EntityManager);
|
||||
var fromUserCoords = _transformSystem.WithEntityId(coords, player);
|
||||
var userCoords = new EntityCoordinates(player, Vector2.Zero);
|
||||
|
||||
|
||||
if (!_transformSystem.InRange(coords, userCoords, range))
|
||||
{
|
||||
var direction = fromUserCoords.Position - userCoords.Position;
|
||||
@@ -150,7 +157,7 @@ public sealed class PullController : VirtualController
|
||||
}
|
||||
|
||||
fromUserCoords = new EntityCoordinates(player, direction.Normalized() * (range - 0.01f));
|
||||
coords = fromUserCoords.WithEntityId(coords.EntityId);
|
||||
coords = _transformSystem.WithEntityId(fromUserCoords, coords.EntityId);
|
||||
}
|
||||
|
||||
var moving = EnsureComp<PullMovingComponent>(pulled!.Value);
|
||||
@@ -241,7 +248,7 @@ public sealed class PullController : VirtualController
|
||||
var pullerXform = _xformQuery.Get(puller);
|
||||
var pullerPosition = TransformSystem.GetMapCoordinates(pullerXform);
|
||||
|
||||
var movingTo = mover.MovingTo.ToMap(EntityManager, TransformSystem);
|
||||
var movingTo = TransformSystem.ToMapCoordinates(mover.MovingTo);
|
||||
|
||||
if (movingTo.MapId != pullerPosition.MapId)
|
||||
{
|
||||
@@ -257,6 +264,13 @@ public sealed class PullController : VirtualController
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: This whole thing is slop and really needs to be throwing again
|
||||
if (TryComp(pullableEnt, out ConveyedComponent? conveyed) && conveyed.Conveying)
|
||||
{
|
||||
RemCompDeferred<PullMovingComponent>(pullableEnt);
|
||||
continue;
|
||||
}
|
||||
|
||||
var movingPosition = movingTo.Position;
|
||||
var ownerPosition = TransformSystem.GetWorldPosition(pullableXform);
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.NPC.Queries.Considerations;
|
||||
|
||||
/// <summary>
|
||||
/// Returns 1f if the target has the <see cref="StunnedComponent"/>
|
||||
/// </summary>
|
||||
public sealed partial class TargetIsStunnedCon : UtilityConsideration
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Turrets;
|
||||
using Content.Shared.Weapons.Melee;
|
||||
@@ -360,6 +361,10 @@ public sealed class NPCUtilitySystem : EntitySystem
|
||||
return 1f;
|
||||
return 0f;
|
||||
}
|
||||
case TargetIsStunnedCon:
|
||||
{
|
||||
return HasComp<StunnedComponent>(targetUid) ? 1f : 0f;
|
||||
}
|
||||
case TurretTargetingCon:
|
||||
{
|
||||
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.DeviceLinking.Events;
|
||||
using Content.Server.DeviceLinking.Systems;
|
||||
using Content.Server.Materials;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Conveyor;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.Maps;
|
||||
@@ -10,7 +9,6 @@ using Content.Shared.Physics.Controllers;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Content.Server.Physics.Controllers;
|
||||
@@ -20,7 +18,6 @@ public sealed class ConveyorController : SharedConveyorController
|
||||
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||
[Dependency] private readonly MaterialReclaimerSystem _materialReclaimer = default!;
|
||||
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -40,7 +37,7 @@ public sealed class ConveyorController : SharedConveyorController
|
||||
{
|
||||
_signalSystem.EnsureSinkPorts(uid, component.ReversePort, component.ForwardPort, component.OffPort);
|
||||
|
||||
if (TryComp<PhysicsComponent>(uid, out var physics))
|
||||
if (PhysicsQuery.TryComp(uid, out var physics))
|
||||
{
|
||||
var shape = new PolygonShape();
|
||||
shape.SetAsBox(0.55f, 0.55f);
|
||||
@@ -57,7 +54,7 @@ public sealed class ConveyorController : SharedConveyorController
|
||||
if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
|
||||
return;
|
||||
|
||||
if (!TryComp<PhysicsComponent>(uid, out var physics))
|
||||
if (!PhysicsQuery.TryComp(uid, out var physics))
|
||||
return;
|
||||
|
||||
_fixtures.DestroyFixture(uid, ConveyorFixture, body: physics);
|
||||
@@ -87,13 +84,11 @@ public sealed class ConveyorController : SharedConveyorController
|
||||
|
||||
else if (args.Port == component.ForwardPort)
|
||||
{
|
||||
AwakenEntities(uid, component);
|
||||
SetState(uid, ConveyorState.Forward, component);
|
||||
}
|
||||
|
||||
else if (args.Port == component.ReversePort)
|
||||
{
|
||||
AwakenEntities(uid, component);
|
||||
SetState(uid, ConveyorState.Reverse, component);
|
||||
}
|
||||
}
|
||||
@@ -108,8 +103,10 @@ public sealed class ConveyorController : SharedConveyorController
|
||||
|
||||
component.State = state;
|
||||
|
||||
if (TryComp<PhysicsComponent>(uid, out var physics))
|
||||
_broadphase.RegenerateContacts((uid, physics));
|
||||
if (state != ConveyorState.Off)
|
||||
{
|
||||
WakeConveyed(uid);
|
||||
}
|
||||
|
||||
UpdateAppearance(uid, component);
|
||||
Dirty(uid, component);
|
||||
@@ -117,29 +114,29 @@ public sealed class ConveyorController : SharedConveyorController
|
||||
|
||||
/// <summary>
|
||||
/// Awakens sleeping entities on the conveyor belt's tile when it's turned on.
|
||||
/// Fixes an issue where non-hard/sleeping entities refuse to wake up + collide if a belt is turned off and on again.
|
||||
/// Need this as we might activate under CollisionWake entities and need to forcefully check them.
|
||||
/// </summary>
|
||||
private void AwakenEntities(EntityUid uid, ConveyorComponent component)
|
||||
protected override void AwakenConveyor(Entity<TransformComponent?> ent)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var bodyQuery = GetEntityQuery<PhysicsComponent>();
|
||||
|
||||
if (!xformQuery.TryGetComponent(uid, out var xform))
|
||||
if (!XformQuery.Resolve(ent.Owner, ref ent.Comp))
|
||||
return;
|
||||
|
||||
var xform = ent.Comp;
|
||||
|
||||
var beltTileRef = xform.Coordinates.GetTileRef(EntityManager, MapManager);
|
||||
|
||||
if (beltTileRef != null)
|
||||
{
|
||||
var intersecting = Lookup.GetLocalEntitiesIntersecting(beltTileRef.Value, 0f);
|
||||
Intersecting.Clear();
|
||||
Lookup.GetLocalEntitiesIntersecting(beltTileRef.Value.GridUid, beltTileRef.Value.GridIndices, Intersecting, 0f, flags: LookupFlags.Dynamic | LookupFlags.Sundries | LookupFlags.Approximate);
|
||||
|
||||
foreach (var entity in intersecting)
|
||||
foreach (var entity in Intersecting)
|
||||
{
|
||||
if (!bodyQuery.TryGetComponent(entity, out var physics))
|
||||
if (!PhysicsQuery.TryGetComponent(entity, out var physics))
|
||||
continue;
|
||||
|
||||
if (physics.BodyType != BodyType.Static)
|
||||
Physics.WakeBody(entity, body: physics);
|
||||
PhysicsSystem.WakeBody(entity, body: physics);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using Content.Server.Polymorph.Systems;
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Polymorph.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
[Access(typeof(PolymorphSystem))]
|
||||
public sealed partial class PolymorphOnCollideComponent : Component
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<PolymorphPrototype> Polymorph;
|
||||
|
||||
[DataField(required: true)]
|
||||
public EntityWhitelist Whitelist = default!;
|
||||
|
||||
[DataField]
|
||||
public EntityWhitelist? Blacklist;
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Magic/forcewall.ogg");
|
||||
}
|
||||
@@ -21,6 +21,7 @@ namespace Content.Server.Rotatable
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -112,7 +113,7 @@ namespace Content.Server.Rotatable
|
||||
var entity = EntityManager.SpawnEntity(component.MirrorEntity, oldTransform.Coordinates);
|
||||
var newTransform = EntityManager.GetComponent<TransformComponent>(entity);
|
||||
newTransform.LocalRotation = oldTransform.LocalRotation;
|
||||
newTransform.Anchored = false;
|
||||
_transform.Unanchor(entity, newTransform);
|
||||
EntityManager.DeleteEntity(uid);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,10 @@ public sealed class HandTeleporterSystem : EntitySystem
|
||||
var timeout = EnsureComp<PortalTimeoutComponent>(user);
|
||||
timeout.EnteredPortal = null;
|
||||
component.FirstPortal = Spawn(component.FirstPortalPrototype, Transform(user).Coordinates);
|
||||
|
||||
if (component.AllowPortalsOnDifferentMaps && TryComp<PortalComponent>(component.FirstPortal, out var portal))
|
||||
portal.CanTeleportToOtherMaps = true;
|
||||
|
||||
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(user):player} opened {ToPrettyString(component.FirstPortal.Value)} at {Transform(component.FirstPortal.Value).Coordinates} using {ToPrettyString(uid)}");
|
||||
_audio.PlayPvs(component.NewPortalSound, uid);
|
||||
}
|
||||
@@ -113,6 +117,10 @@ public sealed class HandTeleporterSystem : EntitySystem
|
||||
var timeout = EnsureComp<PortalTimeoutComponent>(user);
|
||||
timeout.EnteredPortal = null;
|
||||
component.SecondPortal = Spawn(component.SecondPortalPrototype, Transform(user).Coordinates);
|
||||
|
||||
if (component.AllowPortalsOnDifferentMaps && TryComp<PortalComponent>(component.SecondPortal, out var portal))
|
||||
portal.CanTeleportToOtherMaps = true;
|
||||
|
||||
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(user):player} opened {ToPrettyString(component.SecondPortal.Value)} at {Transform(component.SecondPortal.Value).Coordinates} linked to {ToPrettyString(component.FirstPortal!.Value)} using {ToPrettyString(uid)}");
|
||||
_link.TryLink(component.FirstPortal!.Value, component.SecondPortal.Value, true);
|
||||
_audio.PlayPvs(component.NewPortalSound, uid);
|
||||
|
||||
@@ -1,45 +1,41 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Placeable;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Temperature;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.Audio;
|
||||
using Content.Shared.Temperature.Components;
|
||||
using Content.Shared.Temperature.Systems;
|
||||
|
||||
namespace Content.Server.Temperature.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles <see cref="EntityHeaterComponent"/> updating and events.
|
||||
/// Handles the server-only parts of <see cref="SharedEntityHeaterSystem"/>
|
||||
/// </summary>
|
||||
public sealed class EntityHeaterSystem : EntitySystem
|
||||
public sealed class EntityHeaterSystem : SharedEntityHeaterSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly TemperatureSystem _temperature = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
|
||||
private readonly int SettingCount = Enum.GetValues(typeof(EntityHeaterSetting)).Length;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<EntityHeaterComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<EntityHeaterComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
|
||||
SubscribeLocalEvent<EntityHeaterComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<EntityHeaterComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<EntityHeaterComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
// Set initial power level
|
||||
if (TryComp<ApcPowerReceiverComponent>(ent, out var power))
|
||||
power.Load = SettingPower(ent.Comp.Setting, ent.Comp.Power);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<EntityHeaterComponent, ItemPlacerComponent, ApcPowerReceiverComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp, out var placer, out var power))
|
||||
while (query.MoveNext(out _, out _, out var placer, out var power))
|
||||
{
|
||||
if (!power.Powered)
|
||||
continue;
|
||||
|
||||
// don't divide by total entities since its a big grill
|
||||
// don't divide by total entities since it's a big grill
|
||||
// excess would just be wasted in the air but that's not worth simulating
|
||||
// if you want a heater thermomachine just use that...
|
||||
var energy = power.PowerReceived * deltaTime;
|
||||
@@ -50,66 +46,17 @@ public sealed class EntityHeaterSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, EntityHeaterComponent comp, ExaminedEvent args)
|
||||
/// <remarks>
|
||||
/// <see cref="ApcPowerReceiverComponent"/> doesn't exist on the client, so we need
|
||||
/// this server-only override to handle setting the network load.
|
||||
/// </remarks>
|
||||
protected override void ChangeSetting(Entity<EntityHeaterComponent> ent, EntityHeaterSetting setting, EntityUid? user = null)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
base.ChangeSetting(ent, setting, user);
|
||||
|
||||
if (!TryComp<ApcPowerReceiverComponent>(ent, out var power))
|
||||
return;
|
||||
|
||||
args.PushMarkup(Loc.GetString("entity-heater-examined", ("setting", comp.Setting)));
|
||||
}
|
||||
|
||||
private void OnGetVerbs(EntityUid uid, EntityHeaterComponent comp, GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
var setting = (int) comp.Setting;
|
||||
setting++;
|
||||
setting %= SettingCount;
|
||||
var nextSetting = (EntityHeaterSetting) setting;
|
||||
|
||||
args.Verbs.Add(new AlternativeVerb()
|
||||
{
|
||||
Text = Loc.GetString("entity-heater-switch-setting", ("setting", nextSetting)),
|
||||
Act = () =>
|
||||
{
|
||||
ChangeSetting(uid, nextSetting, comp);
|
||||
_popup.PopupEntity(Loc.GetString("entity-heater-switched-setting", ("setting", nextSetting)), uid, args.User);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, EntityHeaterComponent comp, ref PowerChangedEvent args)
|
||||
{
|
||||
// disable heating element glowing layer if theres no power
|
||||
// doesn't actually turn it off since that would be annoying
|
||||
var setting = args.Powered ? comp.Setting : EntityHeaterSetting.Off;
|
||||
_appearance.SetData(uid, EntityHeaterVisuals.Setting, setting);
|
||||
}
|
||||
|
||||
private void ChangeSetting(EntityUid uid, EntityHeaterSetting setting, EntityHeaterComponent? comp = null, ApcPowerReceiverComponent? power = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, ref power))
|
||||
return;
|
||||
|
||||
comp.Setting = setting;
|
||||
power.Load = SettingPower(setting, comp.Power);
|
||||
_appearance.SetData(uid, EntityHeaterVisuals.Setting, setting);
|
||||
_audio.PlayPvs(comp.SettingSound, uid);
|
||||
}
|
||||
|
||||
private float SettingPower(EntityHeaterSetting setting, float max)
|
||||
{
|
||||
switch (setting)
|
||||
{
|
||||
case EntityHeaterSetting.Low:
|
||||
return max / 3f;
|
||||
case EntityHeaterSetting.Medium:
|
||||
return max * 2f / 3f;
|
||||
case EntityHeaterSetting.High:
|
||||
return max;
|
||||
default:
|
||||
return 0f;
|
||||
}
|
||||
power.Load = SettingPower(setting, ent.Comp.Power);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,18 @@ public sealed partial class ThiefUndeterminedBackpackComponent : Component
|
||||
public List<int> SelectedSets = new();
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier ApproveSound = new SoundPathSpecifier("/Audio/Effects/rustle1.ogg");
|
||||
public SoundCollectionSpecifier ApproveSound = new SoundCollectionSpecifier("storageRustle");
|
||||
|
||||
/// <summary>
|
||||
/// Max number of sets you can select.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxSelectedSets = 2;
|
||||
|
||||
/// <summary>
|
||||
/// What entity all the spawned items will appear inside of
|
||||
/// If null, will instead drop on the ground.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId? SpawnedStoragePrototype;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Content.Server.Thief.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Thief;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Audio;
|
||||
@@ -17,6 +19,8 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly SharedStorageSystem _storage = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -37,6 +41,10 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
|
||||
if (backpack.Comp.SelectedSets.Count != backpack.Comp.MaxSelectedSets)
|
||||
return;
|
||||
|
||||
EntityUid? spawnedStorage = null;
|
||||
if (backpack.Comp.SpawnedStoragePrototype != null)
|
||||
spawnedStorage = Spawn(backpack.Comp.SpawnedStoragePrototype, _transform.GetMapCoordinates(backpack.Owner));
|
||||
|
||||
foreach (var i in backpack.Comp.SelectedSets)
|
||||
{
|
||||
var set = _proto.Index(backpack.Comp.PossibleSets[i]);
|
||||
@@ -44,10 +52,20 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
|
||||
{
|
||||
var ent = Spawn(item, _transform.GetMapCoordinates(backpack.Owner));
|
||||
if (TryComp<ItemComponent>(ent, out var itemComponent))
|
||||
_transform.DropNextTo(ent, backpack.Owner);
|
||||
{
|
||||
if (spawnedStorage != null)
|
||||
_storage.Insert(spawnedStorage.Value, ent, out _, playSound: false);
|
||||
else
|
||||
_transform.DropNextTo(ent, backpack.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
_audio.PlayPvs(backpack.Comp.ApproveSound, backpack.Owner);
|
||||
|
||||
if (spawnedStorage != null)
|
||||
_hands.TryPickupAnyHand(args.Actor, spawnedStorage.Value);
|
||||
|
||||
// Play the sound on coordinates of the backpack/toolbox. The reason being, since we immediately delete it, the sound gets deleted alongside it.
|
||||
_audio.PlayPvs(backpack.Comp.ApproveSound, Transform(backpack.Owner).Coordinates);
|
||||
QueueDel(backpack);
|
||||
}
|
||||
private void OnChangeSet(Entity<ThiefUndeterminedBackpackComponent> backpack, ref ThiefBackpackChangeSetMessage args)
|
||||
|
||||
175
Content.Server/Turrets/DeployableTurretSystem.cs
Normal file
175
Content.Server/Turrets/DeployableTurretSystem.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Content.Server.Destructible;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.NPC.HTN;
|
||||
using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Repairable;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Turrets;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Turrets;
|
||||
|
||||
public sealed partial class DeployableTurretSystem : SharedDeployableTurretSystem
|
||||
{
|
||||
[Dependency] private readonly HTNSystem _htn = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DeployableTurretComponent, AmmoShotEvent>(OnAmmoShot);
|
||||
SubscribeLocalEvent<DeployableTurretComponent, ChargeChangedEvent>(OnChargeChanged);
|
||||
SubscribeLocalEvent<DeployableTurretComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<DeployableTurretComponent, BreakageEventArgs>(OnBroken);
|
||||
SubscribeLocalEvent<DeployableTurretComponent, RepairedEvent>(OnRepaired);
|
||||
SubscribeLocalEvent<DeployableTurretComponent, BeforeBroadcastAttemptEvent>(OnBeforeBroadcast);
|
||||
}
|
||||
|
||||
private void OnAmmoShot(Entity<DeployableTurretComponent> ent, ref AmmoShotEvent args)
|
||||
{
|
||||
UpdateAmmoStatus(ent);
|
||||
}
|
||||
|
||||
private void OnChargeChanged(Entity<DeployableTurretComponent> ent, ref ChargeChangedEvent args)
|
||||
{
|
||||
UpdateAmmoStatus(ent);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(Entity<DeployableTurretComponent> ent, ref PowerChangedEvent args)
|
||||
{
|
||||
UpdateAmmoStatus(ent);
|
||||
}
|
||||
|
||||
private void OnBroken(Entity<DeployableTurretComponent> ent, ref BreakageEventArgs args)
|
||||
{
|
||||
if (TryComp<AppearanceComponent>(ent, out var appearance))
|
||||
_appearance.SetData(ent, DeployableTurretVisuals.Broken, true, appearance);
|
||||
|
||||
SetState(ent, false);
|
||||
}
|
||||
|
||||
private void OnRepaired(Entity<DeployableTurretComponent> ent, ref RepairedEvent args)
|
||||
{
|
||||
if (TryComp<AppearanceComponent>(ent, out var appearance))
|
||||
_appearance.SetData(ent, DeployableTurretVisuals.Broken, false, appearance);
|
||||
}
|
||||
|
||||
private void OnBeforeBroadcast(Entity<DeployableTurretComponent> ent, ref BeforeBroadcastAttemptEvent args)
|
||||
{
|
||||
if (!TryComp<DeviceNetworkComponent>(ent, out var deviceNetwork))
|
||||
return;
|
||||
|
||||
var recipientDeviceNetworks = new HashSet<DeviceNetworkComponent>();
|
||||
|
||||
// Only broadcast to connected devices
|
||||
foreach (var recipient in deviceNetwork.DeviceLists)
|
||||
{
|
||||
if (!TryComp<DeviceNetworkComponent>(recipient, out var recipientDeviceNetwork))
|
||||
continue;
|
||||
|
||||
recipientDeviceNetworks.Add(recipientDeviceNetwork);
|
||||
}
|
||||
|
||||
if (recipientDeviceNetworks.Count > 0)
|
||||
args.ModifiedRecipients = recipientDeviceNetworks;
|
||||
}
|
||||
|
||||
private void SendStateUpdateToDeviceNetwork(Entity<DeployableTurretComponent> ent)
|
||||
{
|
||||
if (!TryComp<DeviceNetworkComponent>(ent, out var device))
|
||||
return;
|
||||
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
|
||||
[DeviceNetworkConstants.CmdUpdatedState] = GetTurretState(ent)
|
||||
};
|
||||
|
||||
_deviceNetwork.QueuePacket(ent, null, payload, device: device);
|
||||
}
|
||||
|
||||
protected override void SetState(Entity<DeployableTurretComponent> ent, bool enabled, EntityUid? user = null)
|
||||
{
|
||||
if (ent.Comp.Enabled == enabled)
|
||||
return;
|
||||
|
||||
base.SetState(ent, enabled, user);
|
||||
DirtyField(ent, ent.Comp, nameof(DeployableTurretComponent.Enabled));
|
||||
|
||||
// Determine how much time is remaining in the current animation and the one next in queue
|
||||
var animTimeRemaining = MathF.Max((float)(ent.Comp.AnimationCompletionTime - _timing.CurTime).TotalSeconds, 0f);
|
||||
var animTimeNext = ent.Comp.Enabled ? ent.Comp.DeploymentLength : ent.Comp.RetractionLength;
|
||||
|
||||
// End/restart any tasks the NPC was doing
|
||||
// Delay the resumption of any tasks based on the total animation length (plus a buffer)
|
||||
var planCooldown = animTimeRemaining + animTimeNext + 0.5f;
|
||||
|
||||
if (TryComp<HTNComponent>(ent, out var htn))
|
||||
_htn.SetHTNEnabled((ent, htn), ent.Comp.Enabled, planCooldown);
|
||||
|
||||
// Play audio
|
||||
_audio.PlayPvs(ent.Comp.Enabled ? ent.Comp.DeploymentSound : ent.Comp.RetractionSound, ent, new AudioParams { Volume = -10f });
|
||||
}
|
||||
|
||||
private void UpdateAmmoStatus(Entity<DeployableTurretComponent> ent)
|
||||
{
|
||||
if (!HasAmmo(ent))
|
||||
SetState(ent, false);
|
||||
}
|
||||
|
||||
private DeployableTurretState GetTurretState(Entity<DeployableTurretComponent> ent, DestructibleComponent? destructable = null, HTNComponent? htn = null)
|
||||
{
|
||||
Resolve(ent, ref destructable, ref htn);
|
||||
|
||||
if (destructable?.IsBroken == true)
|
||||
return DeployableTurretState.Broken;
|
||||
|
||||
if (htn == null || !HasAmmo(ent))
|
||||
return DeployableTurretState.Disabled;
|
||||
|
||||
if (htn.Plan?.CurrentTask.Operator is GunOperator)
|
||||
return DeployableTurretState.Firing;
|
||||
|
||||
if (ent.Comp.AnimationCompletionTime > _timing.CurTime)
|
||||
return ent.Comp.Enabled ? DeployableTurretState.Deploying : DeployableTurretState.Retracting;
|
||||
|
||||
return ent.Comp.Enabled ? DeployableTurretState.Deployed : DeployableTurretState.Retracted;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<DeployableTurretComponent, DestructibleComponent, HTNComponent>();
|
||||
while (query.MoveNext(out var uid, out var deployableTurret, out var destructible, out var htn))
|
||||
{
|
||||
// Check if the turret state has changed since the last update,
|
||||
// and if it has, inform the device network
|
||||
var ent = new Entity<DeployableTurretComponent>(uid, deployableTurret);
|
||||
var newState = GetTurretState(ent, destructible, htn);
|
||||
|
||||
if (newState != deployableTurret.CurrentState)
|
||||
{
|
||||
deployableTurret.CurrentState = newState;
|
||||
DirtyField(uid, deployableTurret, nameof(DeployableTurretComponent.CurrentState));
|
||||
|
||||
SendStateUpdateToDeviceNetwork(ent);
|
||||
|
||||
if (TryComp<AppearanceComponent>(ent, out var appearance))
|
||||
_appearance.SetData(ent, DeployableTurretVisuals.Turret, newState, appearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,16 @@ public sealed partial class ZombieSystem
|
||||
melee.Angle = 0.0f;
|
||||
melee.HitSound = zombiecomp.BiteSound;
|
||||
|
||||
DirtyFields(target, melee, null, fields:
|
||||
[
|
||||
nameof(MeleeWeaponComponent.Animation),
|
||||
nameof(MeleeWeaponComponent.WideAnimation),
|
||||
nameof(MeleeWeaponComponent.AltDisarm),
|
||||
nameof(MeleeWeaponComponent.Range),
|
||||
nameof(MeleeWeaponComponent.Angle),
|
||||
nameof(MeleeWeaponComponent.HitSound),
|
||||
]);
|
||||
|
||||
if (mobState.CurrentState == MobState.Alive)
|
||||
{
|
||||
// Groaning when damaged
|
||||
|
||||
@@ -3,7 +3,7 @@ using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Atmos.Components;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||
public sealed partial class GasPressurePumpComponent : Component
|
||||
{
|
||||
[DataField, AutoNetworkedField]
|
||||
|
||||
@@ -5,18 +5,15 @@ using Content.Shared.Atmos.Piping.Binary.Components;
|
||||
using Content.Shared.Atmos.Piping.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.UserInterface;
|
||||
|
||||
namespace Content.Shared.Atmos.EntitySystems;
|
||||
|
||||
public abstract class SharedGasPressurePumpSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
|
||||
[Dependency] protected readonly SharedUserInterfaceSystem UserInterfaceSystem = default!;
|
||||
|
||||
@@ -36,62 +33,71 @@ public abstract class SharedGasPressurePumpSystem : EntitySystem
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, ExaminedEvent>(OnExamined);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, GasPressurePumpComponent pump, ExaminedEvent args)
|
||||
private void OnExamined(Entity<GasPressurePumpComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (!Transform(uid).Anchored)
|
||||
if (!Transform(ent).Anchored)
|
||||
return;
|
||||
|
||||
if (Loc.TryGetString("gas-pressure-pump-system-examined", out var str,
|
||||
if (Loc.TryGetString("gas-pressure-pump-system-examined",
|
||||
out var str,
|
||||
("statusColor", "lightblue"), // TODO: change with pressure?
|
||||
("pressure", pump.TargetPressure)
|
||||
("pressure", ent.Comp.TargetPressure)
|
||||
))
|
||||
{
|
||||
args.PushMarkup(str);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, GasPressurePumpComponent pump, ComponentInit args)
|
||||
private void OnInit(Entity<GasPressurePumpComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
UpdateAppearance(uid, pump);
|
||||
UpdateAppearance(ent);
|
||||
}
|
||||
|
||||
private void OnPowerChanged(EntityUid uid, GasPressurePumpComponent component, ref PowerChangedEvent args)
|
||||
private void OnPowerChanged(Entity<GasPressurePumpComponent> ent, ref PowerChangedEvent args)
|
||||
{
|
||||
UpdateAppearance(uid, component);
|
||||
UpdateAppearance(ent);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid uid, GasPressurePumpComponent? pump = null, AppearanceComponent? appearance = null)
|
||||
private void UpdateAppearance(Entity<GasPressurePumpComponent, AppearanceComponent?> ent)
|
||||
{
|
||||
if (!Resolve(uid, ref pump, ref appearance, false))
|
||||
if (!Resolve(ent, ref ent.Comp2, false))
|
||||
return;
|
||||
|
||||
var pumpOn = pump.Enabled && _receiver.IsPowered(uid);
|
||||
Appearance.SetData(uid, PumpVisuals.Enabled, pumpOn, appearance);
|
||||
var pumpOn = ent.Comp1.Enabled && _receiver.IsPowered(ent.Owner);
|
||||
_appearance.SetData(ent, PumpVisuals.Enabled, pumpOn, ent.Comp2);
|
||||
}
|
||||
|
||||
private void OnToggleStatusMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpToggleStatusMessage args)
|
||||
private void OnToggleStatusMessage(Entity<GasPressurePumpComponent> ent, ref GasPressurePumpToggleStatusMessage args)
|
||||
{
|
||||
pump.Enabled = args.Enabled;
|
||||
_adminLogger.Add(LogType.AtmosPowerChanged, LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(uid):device} to {args.Enabled}");
|
||||
Dirty(uid, pump);
|
||||
UpdateAppearance(uid, pump);
|
||||
ent.Comp.Enabled = args.Enabled;
|
||||
_adminLogger.Add(LogType.AtmosPowerChanged,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(ent):device} to {args.Enabled}");
|
||||
Dirty(ent);
|
||||
UpdateAppearance(ent);
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
private void OnOutputPressureChangeMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpChangeOutputPressureMessage args)
|
||||
private void OnOutputPressureChangeMessage(Entity<GasPressurePumpComponent> ent, ref GasPressurePumpChangeOutputPressureMessage args)
|
||||
{
|
||||
pump.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure);
|
||||
_adminLogger.Add(LogType.AtmosPressureChanged, LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(uid):device} to {args.Pressure}kPa");
|
||||
Dirty(uid, pump);
|
||||
ent.Comp.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure);
|
||||
_adminLogger.Add(LogType.AtmosPressureChanged,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(ent):device} to {args.Pressure}kPa");
|
||||
Dirty(ent);
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
private void OnPumpLeaveAtmosphere(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceDisabledEvent args)
|
||||
private void OnPumpLeaveAtmosphere(Entity<GasPressurePumpComponent> ent, ref AtmosDeviceDisabledEvent args)
|
||||
{
|
||||
pump.Enabled = false;
|
||||
Dirty(uid, pump);
|
||||
UpdateAppearance(uid, pump);
|
||||
ent.Comp.Enabled = false;
|
||||
Dirty(ent);
|
||||
UpdateAppearance(ent);
|
||||
|
||||
UserInterfaceSystem.CloseUi(uid, GasPressurePumpUiKey.Key);
|
||||
UserInterfaceSystem.CloseUi(ent.Owner, GasPressurePumpUiKey.Key);
|
||||
}
|
||||
|
||||
protected virtual void UpdateUi(Entity<GasPressurePumpComponent> ent)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,13 +146,13 @@ public sealed partial class CCVars
|
||||
/// The delay for which two votekicks are allowed to be made by separate people, in seconds.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> VotekickTimeout =
|
||||
CVarDef.Create("votekick.timeout", 120f, CVar.SERVERONLY);
|
||||
CVarDef.Create("votekick.timeout", 60f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the duration of the votekick vote timer.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int>
|
||||
VotekickTimer = CVarDef.Create("votekick.timer", 60, CVar.SERVERONLY);
|
||||
VotekickTimer = CVarDef.Create("votekick.timer", 45, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Config for how many hours playtime a player must have to get protection from the Raider votekick type when playing as an antag.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.EntityTable;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -39,7 +40,8 @@ public sealed class ContainerFillSystem : EntitySystem
|
||||
var ent = Spawn(proto, coords);
|
||||
if (!_containerSystem.Insert(ent, container, containerXform: xform))
|
||||
{
|
||||
Log.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} failed to insert an entity: {ToPrettyString(ent)}.");
|
||||
var alreadyContained = container.ContainedEntities.Count > 0 ? string.Join("\n", container.ContainedEntities.Select(e => $"\t - {EntityManager.ToPrettyString(e)}")) : "< empty >";
|
||||
Log.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} failed to insert an entity: {ToPrettyString(ent)}.\nCurrent contents:\n{alreadyContained}");
|
||||
_transform.AttachToGridOrMap(ent);
|
||||
break;
|
||||
}
|
||||
@@ -72,7 +74,8 @@ public sealed class ContainerFillSystem : EntitySystem
|
||||
var spawn = Spawn(proto, coords);
|
||||
if (!_containerSystem.Insert(spawn, container, containerXform: xform))
|
||||
{
|
||||
Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} failed to insert an entity: {ToPrettyString(spawn)}.");
|
||||
var alreadyContained = container.ContainedEntities.Count > 0 ? string.Join("\n", container.ContainedEntities.Select(e => $"\t - {EntityManager.ToPrettyString(e)}")) : "< empty >";
|
||||
Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} failed to insert an entity: {ToPrettyString(spawn)}.\nCurrent contents:\n{alreadyContained}");
|
||||
_transform.AttachToGridOrMap(spawn);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ using Robust.Shared.GameStates;
|
||||
namespace Content.Shared.Conveyor;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates this entity is currently being conveyed.
|
||||
/// Indicates this entity is currently contacting a conveyor and will subscribe to events as appropriate.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class ConveyedComponent : Component
|
||||
{
|
||||
[ViewVariables, AutoNetworkedField]
|
||||
public List<EntityUid> Colliding = new();
|
||||
// TODO: Delete if pulling gets fixed.
|
||||
/// <summary>
|
||||
/// True if currently conveying.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Conveying;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Shared.Administration.Components;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
@@ -53,6 +54,7 @@ namespace Content.Shared.Cuffs
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly UseDelaySystem _delay = default!;
|
||||
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -717,10 +719,31 @@ namespace Content.Shared.Cuffs
|
||||
}
|
||||
}
|
||||
|
||||
var shoved = false;
|
||||
// if combat mode is on, shove the person.
|
||||
if (_combatMode.IsInCombatMode(user) && target != user && user != null)
|
||||
{
|
||||
var eventArgs = new DisarmedEvent { Target = target, Source = user.Value, PushProbability = 1};
|
||||
RaiseLocalEvent(target, eventArgs);
|
||||
shoved = true;
|
||||
}
|
||||
|
||||
if (cuffable.CuffedHandCount == 0)
|
||||
{
|
||||
if (user != null)
|
||||
_popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user.Value, user.Value);
|
||||
{
|
||||
if (shoved)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-push-success-message",
|
||||
("otherName", Identity.Name(user.Value, EntityManager, user))),
|
||||
user.Value,
|
||||
user.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user.Value, user.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (target != user && user != null)
|
||||
{
|
||||
|
||||
@@ -44,4 +44,46 @@ public sealed partial class DamageOnInteractComponent : Component
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool IsDamageActive = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the thing should be thrown from its current position when they interact with the entity
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Throw = false;
|
||||
|
||||
/// <summary>
|
||||
/// The speed applied to the thing when it is thrown
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int ThrowSpeed = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Time between being able to interact with this entity
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public uint InteractTimer = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the last time this entity was interacted with, but only if the interaction resulted in the user taking damage
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan LastInteraction = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the time that this entity can be interacted with, but only if the interaction resulted in the user taking damage
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan NextInteraction = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Probability that the user will be stunned when they interact with with this entity and took damage
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float StunChance = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Duration, in seconds, of the stun applied to the user when they interact with the entity and took damage
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float StunSeconds = 0.0f;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ using Content.Shared.Database;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Random;
|
||||
using Content.Shared.Throwing;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Effects;
|
||||
using Content.Shared.Stunnable;
|
||||
|
||||
namespace Content.Shared.Damage.Systems;
|
||||
|
||||
@@ -17,6 +23,10 @@ public sealed class DamageOnInteractSystem : EntitySystem
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly SharedStunSystem _stun = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -35,6 +45,13 @@ public sealed class DamageOnInteractSystem : EntitySystem
|
||||
/// <param name="args">Contains the user that interacted with the entity</param>
|
||||
private void OnHandInteract(Entity<DamageOnInteractComponent> entity, ref InteractHandEvent args)
|
||||
{
|
||||
// Stop the interaction if the user attempts to interact with the object before the timer is finished
|
||||
if (_gameTiming.CurTime < entity.Comp.NextInteraction)
|
||||
{
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entity.Comp.IsDamageActive)
|
||||
return;
|
||||
|
||||
@@ -47,9 +64,8 @@ public sealed class DamageOnInteractSystem : EntitySystem
|
||||
|
||||
// or checking the entity for the comp itself if the inventory didn't work
|
||||
if (protectiveEntity.Comp == null && TryComp<DamageOnInteractProtectionComponent>(args.User, out var protectiveComp))
|
||||
{
|
||||
protectiveEntity = (args.User, protectiveComp);
|
||||
}
|
||||
|
||||
|
||||
// if protectiveComp isn't null after all that, it means the user has protection,
|
||||
// so let's calculate how much they resist
|
||||
@@ -59,17 +75,31 @@ public sealed class DamageOnInteractSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target);
|
||||
totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target);
|
||||
|
||||
if (totalDamage != null && totalDamage.AnyPositive())
|
||||
{
|
||||
// Record this interaction and determine when a user is allowed to interact with this entity again
|
||||
entity.Comp.LastInteraction = _gameTiming.CurTime;
|
||||
entity.Comp.NextInteraction = _gameTiming.CurTime + TimeSpan.FromSeconds(entity.Comp.InteractTimer);
|
||||
|
||||
args.Handled = true;
|
||||
_adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} injured their hand by interacting with {ToPrettyString(args.Target):target} and received {totalDamage.GetTotal():damage} damage");
|
||||
_audioSystem.PlayPredicted(entity.Comp.InteractSound, args.Target, args.User);
|
||||
|
||||
if (entity.Comp.PopupText != null)
|
||||
_popupSystem.PopupClient(Loc.GetString(entity.Comp.PopupText), args.User, args.User);
|
||||
|
||||
// Attempt to paralyze the user after they have taken damage
|
||||
if (_random.Prob(entity.Comp.StunChance))
|
||||
_stun.TryParalyze(args.User, TimeSpan.FromSeconds(entity.Comp.StunSeconds), true);
|
||||
}
|
||||
// Check if the entity's Throw bool is false, or if the entity has the PullableComponent, then if the entity is currently being pulled.
|
||||
// BeingPulled must be checked because the entity will be spastically thrown around without this.
|
||||
if (!entity.Comp.Throw || !TryComp<PullableComponent>(entity, out var pullComp) || pullComp.BeingPulled)
|
||||
return;
|
||||
|
||||
_throwingSystem.TryThrow(entity, _random.NextVector2(), entity.Comp.ThrowSpeed, doSpin: true);
|
||||
}
|
||||
|
||||
public void SetIsDamageActiveTo(Entity<DamageOnInteractComponent> entity, bool mode)
|
||||
|
||||
@@ -296,7 +296,7 @@ namespace Content.Shared.Damage
|
||||
DamageChanged(uid, component, new DamageSpecifier());
|
||||
}
|
||||
|
||||
public void SetDamageModifierSetId(EntityUid uid, string damageModifierSetId, DamageableComponent? comp = null)
|
||||
public void SetDamageModifierSetId(EntityUid uid, string? damageModifierSetId, DamageableComponent? comp = null)
|
||||
{
|
||||
if (!_damageableQuery.Resolve(uid, ref comp))
|
||||
return;
|
||||
|
||||
@@ -67,6 +67,8 @@ public partial class InventorySystem
|
||||
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);
|
||||
|
||||
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
|
||||
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<InnateVerb>>(OnGetInnateVerbs);
|
||||
|
||||
}
|
||||
|
||||
protected void RefRelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent
|
||||
@@ -121,6 +123,17 @@ public partial class InventorySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGetInnateVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<InnateVerb> args)
|
||||
{
|
||||
// Automatically relay stripping related verbs to all equipped clothing.
|
||||
var ev = new InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>(args);
|
||||
var enumerator = new InventorySlotEnumerator(component, SlotFlags.WITHOUT_POCKET);
|
||||
while (enumerator.NextItem(out var item))
|
||||
{
|
||||
RaiseLocalEvent(item, ev);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Content.Shared.Maps
|
||||
return null;
|
||||
|
||||
mapManager ??= IoCManager.Resolve<IMapManager>();
|
||||
var pos = coordinates.ToMap(entityManager, entityManager.System<SharedTransformSystem>());
|
||||
var pos = entityManager.System<SharedTransformSystem>().ToMapCoordinates(coordinates);
|
||||
if (!mapManager.TryFindGridAt(pos, out _, out var grid))
|
||||
return null;
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Medical.Stethoscope.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a verb and action that allows the user to listen to the entity's breathing.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class StethoscopeComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Time between each use of the stethoscope.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(1.75);
|
||||
|
||||
/// <summary>
|
||||
/// Last damage that was measured. Used to indicate if breathing is improving or getting worse.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2? LastMeasuredDamage;
|
||||
|
||||
[DataField]
|
||||
public EntProtoId Action = "ActionStethoscope";
|
||||
|
||||
[DataField]
|
||||
public EntityUid? ActionEntity;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Actions;
|
||||
|
||||
namespace Content.Shared.Medical.Stethoscope;
|
||||
|
||||
public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent
|
||||
{
|
||||
}
|
||||
148
Content.Shared/Medical/Stethoscope/StethoscopeSystem.cs
Normal file
148
Content.Shared/Medical/Stethoscope/StethoscopeSystem.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Medical.Stethoscope.Components;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Shared.Medical.Stethoscope;
|
||||
|
||||
public sealed class StethoscopeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
|
||||
// The damage type to "listen" for with the stethoscope.
|
||||
private const string DamageToListenFor = "Asphyxiation";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StethoscopeComponent, InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>>(AddStethoscopeVerb);
|
||||
SubscribeLocalEvent<StethoscopeComponent, GetItemActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<StethoscopeComponent, StethoscopeActionEvent>(OnStethoscopeAction);
|
||||
SubscribeLocalEvent<StethoscopeComponent, StethoscopeDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
|
||||
private void OnGetActions(Entity<StethoscopeComponent> ent, ref GetItemActionsEvent args)
|
||||
{
|
||||
args.AddAction(ref ent.Comp.ActionEntity, ent.Comp.Action);
|
||||
}
|
||||
|
||||
private void OnStethoscopeAction(Entity<StethoscopeComponent> ent, ref StethoscopeActionEvent args)
|
||||
{
|
||||
StartListening(ent, args.Target);
|
||||
}
|
||||
|
||||
private void AddStethoscopeVerb(Entity<StethoscopeComponent> ent, ref InventoryRelayedEvent<GetVerbsEvent<InnateVerb>> args)
|
||||
{
|
||||
if (!args.Args.CanInteract || !args.Args.CanAccess)
|
||||
return;
|
||||
|
||||
if (!HasComp<MobStateComponent>(args.Args.Target))
|
||||
return;
|
||||
|
||||
var target = args.Args.Target;
|
||||
|
||||
InnateVerb verb = new()
|
||||
{
|
||||
Act = () => StartListening(ent, target),
|
||||
Text = Loc.GetString("stethoscope-verb"),
|
||||
IconEntity = GetNetEntity(ent),
|
||||
Priority = 2,
|
||||
};
|
||||
args.Args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void StartListening(Entity<StethoscopeComponent> ent, EntityUid target)
|
||||
{
|
||||
if (!_container.TryGetContainingContainer((ent, null, null), out var container))
|
||||
return;
|
||||
|
||||
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, container.Owner, ent.Comp.Delay, new StethoscopeDoAfterEvent(), ent, target: target, used: ent)
|
||||
{
|
||||
DuplicateCondition = DuplicateConditions.SameEvent,
|
||||
BreakOnMove = true,
|
||||
Hidden = true,
|
||||
BreakOnHandChange = false,
|
||||
});
|
||||
}
|
||||
|
||||
private void OnDoAfter(Entity<StethoscopeComponent> ent, ref StethoscopeDoAfterEvent args)
|
||||
{
|
||||
var target = args.Target;
|
||||
|
||||
if (args.Handled || target == null || args.Cancelled)
|
||||
{
|
||||
ent.Comp.LastMeasuredDamage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
ExamineWithStethoscope(ent, args.Args.User, target.Value);
|
||||
|
||||
args.Repeat = true;
|
||||
}
|
||||
|
||||
private void ExamineWithStethoscope(Entity<StethoscopeComponent> stethoscope, EntityUid user, EntityUid target)
|
||||
{
|
||||
// TODO: Add check for respirator component when it gets moved to shared.
|
||||
// If the mob is dead or cannot asphyxiation damage, the popup shows nothing.
|
||||
if (!TryComp<MobStateComponent>(target, out var mobState) ||
|
||||
!TryComp<DamageableComponent>(target, out var damageComp) ||
|
||||
_mobState.IsDead(target, mobState) ||
|
||||
!damageComp.Damage.DamageDict.TryGetValue(DamageToListenFor, out var asphyxDmg))
|
||||
{
|
||||
_popup.PopupPredicted(Loc.GetString("stethoscope-nothing"), target, user);
|
||||
stethoscope.Comp.LastMeasuredDamage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var absString = GetAbsoluteDamageString(asphyxDmg);
|
||||
|
||||
// Don't show the change if this is the first time listening.
|
||||
if (stethoscope.Comp.LastMeasuredDamage == null)
|
||||
{
|
||||
_popup.PopupPredicted(absString, target, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
var deltaString = GetDeltaDamageString(stethoscope.Comp.LastMeasuredDamage.Value, asphyxDmg);
|
||||
_popup.PopupPredicted(Loc.GetString("stethoscope-combined-status", ("absolute", absString), ("delta", deltaString)), target, user);
|
||||
}
|
||||
|
||||
stethoscope.Comp.LastMeasuredDamage = asphyxDmg;
|
||||
}
|
||||
|
||||
private string GetAbsoluteDamageString(FixedPoint2 asphyxDmg)
|
||||
{
|
||||
var msg = (int) asphyxDmg switch
|
||||
{
|
||||
< 10 => "stethoscope-normal",
|
||||
< 30 => "stethoscope-raggedy",
|
||||
< 60 => "stethoscope-hyper",
|
||||
< 80 => "stethoscope-irregular",
|
||||
_ => "stethoscope-fucked",
|
||||
};
|
||||
return Loc.GetString(msg);
|
||||
}
|
||||
|
||||
private string GetDeltaDamageString(FixedPoint2 lastDamage, FixedPoint2 currentDamage)
|
||||
{
|
||||
if (lastDamage > currentDamage)
|
||||
return Loc.GetString("stethoscope-delta-improving");
|
||||
if (lastDamage < currentDamage)
|
||||
return Loc.GetString("stethoscope-delta-worsening");
|
||||
return Loc.GetString("stethoscope-delta-steady");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent;
|
||||
@@ -4,6 +4,4 @@ using Robust.Shared.Serialization;
|
||||
namespace Content.Shared.Medical;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent
|
||||
{
|
||||
}
|
||||
public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Movement.Components;
|
||||
|
||||
@@ -9,5 +10,10 @@ namespace Content.Shared.Movement.Components;
|
||||
public sealed partial class ActiveJetpackComponent : Component
|
||||
{
|
||||
public float EffectCooldown = 0.3f;
|
||||
|
||||
public float MaxDistance = 0.7f;
|
||||
|
||||
public EntityCoordinates LastCoordinates;
|
||||
|
||||
public TimeSpan TargetTime = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Content.Shared.Movement.Components
|
||||
/// <summary>
|
||||
/// Should our velocity be applied to our parent?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("toParent")]
|
||||
[DataField]
|
||||
public bool ToParent = false;
|
||||
|
||||
public GameTick LastInputTick;
|
||||
@@ -43,6 +43,12 @@ namespace Content.Shared.Movement.Components
|
||||
|
||||
public MoveButtons HeldMoveButtons = MoveButtons.None;
|
||||
|
||||
// I don't know if we even need this networked? It's mostly so conveyors can calculate properly.
|
||||
/// <summary>
|
||||
/// Direction to move this tick.
|
||||
/// </summary>
|
||||
public Vector2 WishDir;
|
||||
|
||||
/// <summary>
|
||||
/// Entity our movement is relative to.
|
||||
/// </summary>
|
||||
@@ -65,7 +71,6 @@ namespace Content.Shared.Movement.Components
|
||||
/// If we traverse on / off a grid then set a timer to update our relative inputs.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan LerpTarget;
|
||||
|
||||
public const float LerpTime = 1.0f;
|
||||
|
||||
@@ -155,7 +155,6 @@ public abstract partial class SharedMoverController : VirtualController
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
UsedMobMovement[uid] = true;
|
||||
// Specifically don't use mover.Owner because that may be different to the actual physics body being moved.
|
||||
var weightless = _gravity.IsWeightless(physicsUid, physicsComponent, xform);
|
||||
@@ -203,20 +202,21 @@ public abstract partial class SharedMoverController : VirtualController
|
||||
var total = walkDir * walkSpeed + sprintDir * sprintSpeed;
|
||||
|
||||
var parentRotation = GetParentGridAngle(mover);
|
||||
var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total;
|
||||
var wishDir = _relativeMovement ? parentRotation.RotateVec(total) : total;
|
||||
|
||||
DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), worldTotal.Length()));
|
||||
DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), wishDir.Length()));
|
||||
|
||||
var velocity = physicsComponent.LinearVelocity;
|
||||
float friction;
|
||||
float weightlessModifier;
|
||||
float accel;
|
||||
var velocity = physicsComponent.LinearVelocity;
|
||||
|
||||
// Whether we use weightless friction or not.
|
||||
if (weightless)
|
||||
{
|
||||
if (gridComp == null && !MapGridQuery.HasComp(xform.GridUid))
|
||||
friction = moveSpeedComponent?.OffGridFriction ?? MovementSpeedModifierComponent.DefaultOffGridFriction;
|
||||
else if (worldTotal != Vector2.Zero && touching)
|
||||
else if (wishDir != Vector2.Zero && touching)
|
||||
friction = moveSpeedComponent?.WeightlessFriction ?? MovementSpeedModifierComponent.DefaultWeightlessFriction;
|
||||
else
|
||||
friction = moveSpeedComponent?.WeightlessFrictionNoInput ?? MovementSpeedModifierComponent.DefaultWeightlessFrictionNoInput;
|
||||
@@ -226,7 +226,7 @@ public abstract partial class SharedMoverController : VirtualController
|
||||
}
|
||||
else
|
||||
{
|
||||
if (worldTotal != Vector2.Zero || moveSpeedComponent?.FrictionNoInput == null)
|
||||
if (wishDir != Vector2.Zero || moveSpeedComponent?.FrictionNoInput == null)
|
||||
{
|
||||
friction = tileDef?.MobFriction ?? moveSpeedComponent?.Friction ?? MovementSpeedModifierComponent.DefaultFriction;
|
||||
}
|
||||
@@ -242,14 +242,27 @@ public abstract partial class SharedMoverController : VirtualController
|
||||
var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed;
|
||||
Friction(minimumFrictionSpeed, frameTime, friction, ref velocity);
|
||||
|
||||
if (worldTotal != Vector2.Zero)
|
||||
wishDir *= weightlessModifier;
|
||||
|
||||
if (!weightless || touching)
|
||||
Accelerate(ref velocity, in wishDir, accel, frameTime);
|
||||
|
||||
SetWishDir((uid, mover), wishDir);
|
||||
|
||||
PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent);
|
||||
|
||||
// Ensures that players do not spiiiiiiin
|
||||
PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent);
|
||||
|
||||
// Handle footsteps at the end
|
||||
if (total != Vector2.Zero)
|
||||
{
|
||||
if (!NoRotateQuery.HasComponent(uid))
|
||||
{
|
||||
// TODO apparently this results in a duplicate move event because "This should have its event run during
|
||||
// island solver"??. So maybe SetRotation needs an argument to avoid raising an event?
|
||||
var worldRot = _transform.GetWorldRotation(xform);
|
||||
_transform.SetLocalRotation(xform, xform.LocalRotation + worldTotal.ToWorldAngle() - worldRot);
|
||||
_transform.SetLocalRotation(xform, xform.LocalRotation + wishDir.ToWorldAngle() - worldRot);
|
||||
}
|
||||
|
||||
if (!weightless && MobMoverQuery.TryGetComponent(uid, out var mobMover) &&
|
||||
@@ -272,16 +285,23 @@ public abstract partial class SharedMoverController : VirtualController
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
worldTotal *= weightlessModifier;
|
||||
public Vector2 GetWishDir(Entity<InputMoverComponent?> mover)
|
||||
{
|
||||
if (!MoverQuery.Resolve(mover.Owner, ref mover.Comp, false))
|
||||
return Vector2.Zero;
|
||||
|
||||
if (!weightless || touching)
|
||||
Accelerate(ref velocity, in worldTotal, accel, frameTime);
|
||||
return mover.Comp.WishDir;
|
||||
}
|
||||
|
||||
PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent);
|
||||
public void SetWishDir(Entity<InputMoverComponent> mover, Vector2 wishDir)
|
||||
{
|
||||
if (mover.Comp.WishDir.Equals(wishDir))
|
||||
return;
|
||||
|
||||
// Ensures that players do not spiiiiiiin
|
||||
PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent);
|
||||
mover.Comp.WishDir = wishDir;
|
||||
Dirty(mover);
|
||||
}
|
||||
|
||||
public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTime)
|
||||
@@ -317,7 +337,7 @@ public abstract partial class SharedMoverController : VirtualController
|
||||
}
|
||||
}
|
||||
|
||||
private void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity)
|
||||
public void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity)
|
||||
{
|
||||
var speed = velocity.Length();
|
||||
|
||||
@@ -338,7 +358,10 @@ public abstract partial class SharedMoverController : VirtualController
|
||||
velocity *= newSpeed;
|
||||
}
|
||||
|
||||
private void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime)
|
||||
/// <summary>
|
||||
/// Adjusts the current velocity to the target velocity based on the specified acceleration.
|
||||
/// </summary>
|
||||
public static void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime)
|
||||
{
|
||||
var wishDir = velocity != Vector2.Zero ? velocity.Normalized() : Vector2.Zero;
|
||||
var wishSpeed = velocity.Length();
|
||||
|
||||
@@ -2,124 +2,211 @@
|
||||
using Content.Shared.Conveyor;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Magic;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Controllers;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Threading;
|
||||
|
||||
namespace Content.Shared.Physics.Controllers;
|
||||
|
||||
public abstract class SharedConveyorController : VirtualController
|
||||
{
|
||||
[Dependency] protected readonly IMapManager MapManager = default!;
|
||||
[Dependency] private readonly IParallelManager _parallel = default!;
|
||||
[Dependency] private readonly CollisionWakeSystem _wake = default!;
|
||||
[Dependency] protected readonly EntityLookupSystem Lookup = default!;
|
||||
[Dependency] private readonly SharedMapSystem _maps = default!;
|
||||
[Dependency] protected readonly SharedPhysicsSystem Physics = default!;
|
||||
[Dependency] private readonly SharedGravitySystem _gravity = default!;
|
||||
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
||||
[Dependency] private readonly SharedGravitySystem _gravity = default!;
|
||||
[Dependency] private readonly SharedMoverController _mover = default!;
|
||||
|
||||
protected const string ConveyorFixture = "conveyor";
|
||||
|
||||
private EntityQuery<MapGridComponent> _gridQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
private ConveyorJob _job;
|
||||
|
||||
private ValueList<EntityUid> _ents = new();
|
||||
private HashSet<Entity<ConveyorComponent>> _conveyors = new();
|
||||
private EntityQuery<ConveyorComponent> _conveyorQuery;
|
||||
private EntityQuery<ConveyedComponent> _conveyedQuery;
|
||||
protected EntityQuery<PhysicsComponent> PhysicsQuery;
|
||||
protected EntityQuery<TransformComponent> XformQuery;
|
||||
|
||||
protected HashSet<EntityUid> Intersecting = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_gridQuery = GetEntityQuery<MapGridComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
_job = new ConveyorJob(this);
|
||||
_conveyorQuery = GetEntityQuery<ConveyorComponent>();
|
||||
_conveyedQuery = GetEntityQuery<ConveyedComponent>();
|
||||
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
XformQuery = GetEntityQuery<TransformComponent>();
|
||||
|
||||
UpdatesAfter.Add(typeof(SharedMoverController));
|
||||
|
||||
SubscribeLocalEvent<ConveyedComponent, TileFrictionEvent>(OnConveyedFriction);
|
||||
SubscribeLocalEvent<ConveyedComponent, ComponentStartup>(OnConveyedStartup);
|
||||
SubscribeLocalEvent<ConveyedComponent, ComponentShutdown>(OnConveyedShutdown);
|
||||
|
||||
SubscribeLocalEvent<ConveyorComponent, StartCollideEvent>(OnConveyorStartCollide);
|
||||
SubscribeLocalEvent<ConveyorComponent, EndCollideEvent>(OnConveyorEndCollide);
|
||||
SubscribeLocalEvent<ConveyorComponent, ComponentStartup>(OnConveyorStartup);
|
||||
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
private void OnConveyorStartCollide(EntityUid uid, ConveyorComponent component, ref StartCollideEvent args)
|
||||
private void OnConveyedFriction(Entity<ConveyedComponent> ent, ref TileFrictionEvent args)
|
||||
{
|
||||
// Conveyed entities don't get friction, they just get wishdir applied so will inherently slowdown anyway.
|
||||
args.Modifier = 0f;
|
||||
}
|
||||
|
||||
private void OnConveyedStartup(Entity<ConveyedComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
// We need waking / sleeping to work and don't want collisionwake interfering with us.
|
||||
_wake.SetEnabled(ent.Owner, false);
|
||||
}
|
||||
|
||||
private void OnConveyedShutdown(Entity<ConveyedComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_wake.SetEnabled(ent.Owner, true);
|
||||
}
|
||||
|
||||
private void OnConveyorStartup(Entity<ConveyorComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
AwakenConveyor(ent.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forcefully awakens all entities near the conveyor.
|
||||
/// </summary>
|
||||
protected virtual void AwakenConveyor(Entity<TransformComponent?> ent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wakes all conveyed entities contacting this conveyor.
|
||||
/// </summary>
|
||||
protected void WakeConveyed(EntityUid conveyorUid)
|
||||
{
|
||||
var contacts = PhysicsSystem.GetContacts(conveyorUid);
|
||||
|
||||
while (contacts.MoveNext(out var contact))
|
||||
{
|
||||
var other = contact.OtherEnt(conveyorUid);
|
||||
|
||||
if (_conveyedQuery.HasComp(other))
|
||||
{
|
||||
PhysicsSystem.WakeBody(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConveyorStartCollide(Entity<ConveyorComponent> conveyor, ref StartCollideEvent args)
|
||||
{
|
||||
var otherUid = args.OtherEntity;
|
||||
|
||||
if (!args.OtherFixture.Hard || args.OtherBody.BodyType == BodyType.Static || component.State == ConveyorState.Off)
|
||||
if (!args.OtherFixture.Hard || args.OtherBody.BodyType == BodyType.Static)
|
||||
return;
|
||||
|
||||
var conveyed = EnsureComp<ConveyedComponent>(otherUid);
|
||||
|
||||
if (conveyed.Colliding.Contains(uid))
|
||||
return;
|
||||
|
||||
conveyed.Colliding.Add(uid);
|
||||
Dirty(otherUid, conveyed);
|
||||
}
|
||||
|
||||
private void OnConveyorEndCollide(Entity<ConveyorComponent> ent, ref EndCollideEvent args)
|
||||
{
|
||||
if (!TryComp(args.OtherEntity, out ConveyedComponent? conveyed))
|
||||
return;
|
||||
|
||||
if (!conveyed.Colliding.Remove(ent.Owner))
|
||||
return;
|
||||
|
||||
Dirty(args.OtherEntity, conveyed);
|
||||
EnsureComp<ConveyedComponent>(otherUid);
|
||||
}
|
||||
|
||||
public override void UpdateBeforeSolve(bool prediction, float frameTime)
|
||||
{
|
||||
base.UpdateBeforeSolve(prediction, frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<ConveyedComponent, TransformComponent, PhysicsComponent>();
|
||||
_ents.Clear();
|
||||
_job.Prediction = prediction;
|
||||
_job.Conveyed.Clear();
|
||||
|
||||
while (query.MoveNext(out var uid, out var comp, out var xform, out var physics))
|
||||
var query = EntityQueryEnumerator<ConveyedComponent, FixturesComponent, PhysicsComponent, TransformComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var comp, out var fixtures, out var physics, out var xform))
|
||||
{
|
||||
if (TryConvey((uid, comp, physics, xform), prediction, frameTime))
|
||||
continue;
|
||||
|
||||
_ents.Add(uid);
|
||||
_job.Conveyed.Add(((uid, comp, fixtures, physics, xform), Vector2.Zero, false));
|
||||
}
|
||||
|
||||
foreach (var ent in _ents)
|
||||
_parallel.ProcessNow(_job, _job.Conveyed.Count);
|
||||
|
||||
foreach (var ent in _job.Conveyed)
|
||||
{
|
||||
RemComp<ConveyedComponent>(ent);
|
||||
if (!ent.Entity.Comp3.Predict && prediction)
|
||||
continue;
|
||||
|
||||
var physics = ent.Entity.Comp3;
|
||||
var velocity = physics.LinearVelocity;
|
||||
var targetDir = ent.Direction;
|
||||
|
||||
// If mob is moving with the conveyor then combine the directions.
|
||||
var wishDir = _mover.GetWishDir(ent.Entity.Owner);
|
||||
|
||||
if (Vector2.Dot(wishDir, targetDir) > 0f)
|
||||
{
|
||||
targetDir += wishDir;
|
||||
}
|
||||
|
||||
if (ent.Result)
|
||||
{
|
||||
SetConveying(ent.Entity.Owner, ent.Entity.Comp1, targetDir.LengthSquared() > 0f);
|
||||
|
||||
// We apply friction here so when we push items towards the center of the conveyor they don't go overspeed.
|
||||
// We also don't want this to apply to mobs as they apply their own friction and otherwise
|
||||
// they'll go too slow.
|
||||
if (!_mover.UsedMobMovement.TryGetValue(ent.Entity.Owner, out var usedMob) || !usedMob)
|
||||
{
|
||||
_mover.Friction(0f, frameTime: frameTime, friction: 5f, ref velocity);
|
||||
}
|
||||
|
||||
SharedMoverController.Accelerate(ref velocity, targetDir, 20f, frameTime);
|
||||
}
|
||||
else if (!_mover.UsedMobMovement.TryGetValue(ent.Entity.Owner, out var usedMob) || !usedMob)
|
||||
{
|
||||
// Need friction to outweigh the movement as it will bounce a bit against the wall.
|
||||
// This facilitates being able to sleep entities colliding into walls.
|
||||
_mover.Friction(0f, frameTime: frameTime, friction: 40f, ref velocity);
|
||||
}
|
||||
|
||||
PhysicsSystem.SetLinearVelocity(ent.Entity.Owner, velocity, wakeBody: false);
|
||||
|
||||
if (!IsConveyed((ent.Entity.Owner, ent.Entity.Comp2)))
|
||||
{
|
||||
RemComp<ConveyedComponent>(ent.Entity.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryConvey(Entity<ConveyedComponent, PhysicsComponent, TransformComponent> entity, bool prediction, float frameTime)
|
||||
private void SetConveying(EntityUid uid, ConveyedComponent conveyed, bool value)
|
||||
{
|
||||
var physics = entity.Comp2;
|
||||
var xform = entity.Comp3;
|
||||
var contacting = entity.Comp1.Colliding.Count > 0;
|
||||
if (conveyed.Conveying == value)
|
||||
return;
|
||||
|
||||
if (!contacting)
|
||||
return false;
|
||||
conveyed.Conveying = value;
|
||||
Dirty(uid, conveyed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conveying direction for an entity.
|
||||
/// </summary>
|
||||
/// <returns>False if we should no longer be considered actively conveyed.</returns>
|
||||
private bool TryConvey(Entity<ConveyedComponent, FixturesComponent, PhysicsComponent, TransformComponent> entity,
|
||||
bool prediction,
|
||||
out Vector2 direction)
|
||||
{
|
||||
direction = Vector2.Zero;
|
||||
var fixtures = entity.Comp2;
|
||||
var physics = entity.Comp3;
|
||||
var xform = entity.Comp4;
|
||||
|
||||
if (!physics.Awake)
|
||||
return true;
|
||||
|
||||
// Client moment
|
||||
if (!physics.Predict && prediction)
|
||||
return true;
|
||||
|
||||
if (physics.BodyType == BodyType.Static)
|
||||
return false;
|
||||
|
||||
if (!_gridQuery.TryComp(xform.GridUid, out var grid))
|
||||
return true;
|
||||
|
||||
var gridTile = _maps.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates);
|
||||
_conveyors.Clear();
|
||||
|
||||
// Check for any conveyors on the attached tile.
|
||||
Lookup.GetLocalEntitiesIntersecting(xform.GridUid.Value, gridTile, _conveyors);
|
||||
DebugTools.Assert(_conveyors.Count <= 1);
|
||||
|
||||
// No more conveyors.
|
||||
if (_conveyors.Count == 0)
|
||||
if (xform.GridUid == null)
|
||||
return true;
|
||||
|
||||
if (physics.BodyStatus == BodyStatus.InAir ||
|
||||
@@ -130,48 +217,93 @@ public abstract class SharedConveyorController : VirtualController
|
||||
|
||||
Entity<ConveyorComponent> bestConveyor = default;
|
||||
var bestSpeed = 0f;
|
||||
var contacts = PhysicsSystem.GetContacts((entity.Owner, fixtures));
|
||||
var transform = PhysicsSystem.GetPhysicsTransform(entity.Owner);
|
||||
var anyConveyors = false;
|
||||
|
||||
foreach (var conveyor in _conveyors)
|
||||
while (contacts.MoveNext(out var contact))
|
||||
{
|
||||
if (conveyor.Comp.Speed > bestSpeed && CanRun(conveyor))
|
||||
if (!contact.IsTouching)
|
||||
continue;
|
||||
|
||||
// Check if our center is over their fixture otherwise ignore it.
|
||||
var other = contact.OtherEnt(entity.Owner);
|
||||
|
||||
// Check for blocked, if so then we can't convey at all and just try to sleep
|
||||
// Otherwise we may just keep pushing it into the wall
|
||||
|
||||
if (!_conveyorQuery.TryComp(other, out var conveyor))
|
||||
continue;
|
||||
|
||||
anyConveyors = true;
|
||||
var otherFixture = contact.OtherFixture(entity.Owner);
|
||||
var otherTransform = PhysicsSystem.GetPhysicsTransform(other);
|
||||
|
||||
// Check if our center is over the conveyor, otherwise ignore it.
|
||||
if (!_fixtures.TestPoint(otherFixture.Item2.Shape, otherTransform, transform.Position))
|
||||
continue;
|
||||
|
||||
if (conveyor.Speed > bestSpeed && CanRun(conveyor))
|
||||
{
|
||||
bestSpeed = conveyor.Comp.Speed;
|
||||
bestConveyor = conveyor;
|
||||
bestSpeed = conveyor.Speed;
|
||||
bestConveyor = (other, conveyor);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have no touching contacts we shouldn't be using conveyed anyway so nuke it.
|
||||
if (!anyConveyors)
|
||||
return true;
|
||||
|
||||
if (bestSpeed == 0f || bestConveyor == default)
|
||||
return true;
|
||||
|
||||
var comp = bestConveyor.Comp!;
|
||||
var conveyorXform = _xformQuery.GetComponent(bestConveyor.Owner);
|
||||
var conveyorPos = conveyorXform.LocalPosition;
|
||||
var conveyorRot = conveyorXform.LocalRotation;
|
||||
var conveyorXform = XformQuery.GetComponent(bestConveyor.Owner);
|
||||
var (conveyorPos, conveyorRot) = TransformSystem.GetWorldPositionRotation(conveyorXform);
|
||||
|
||||
conveyorRot += bestConveyor.Comp!.Angle;
|
||||
|
||||
if (comp.State == ConveyorState.Reverse)
|
||||
conveyorRot += MathF.PI;
|
||||
|
||||
var direction = conveyorRot.ToWorldVec();
|
||||
var conveyorDirection = conveyorRot.ToWorldVec();
|
||||
direction = conveyorDirection;
|
||||
|
||||
var localPos = xform.LocalPosition;
|
||||
var itemRelative = conveyorPos - localPos;
|
||||
var itemRelative = conveyorPos - transform.Position;
|
||||
direction = Convey(direction, bestSpeed, itemRelative);
|
||||
|
||||
localPos += Convey(direction, bestSpeed, frameTime, itemRelative);
|
||||
// Do a final check for hard contacts so if we're conveying into a wall then NOOP.
|
||||
contacts = PhysicsSystem.GetContacts((entity.Owner, fixtures));
|
||||
|
||||
TransformSystem.SetLocalPosition(entity, localPos, xform);
|
||||
while (contacts.MoveNext(out var contact))
|
||||
{
|
||||
if (!contact.Hard || !contact.IsTouching)
|
||||
continue;
|
||||
|
||||
// Force it awake for collisionwake reasons.
|
||||
Physics.SetAwake((entity, physics), true);
|
||||
Physics.SetSleepTime(physics, 0f);
|
||||
var other = contact.OtherEnt(entity.Owner);
|
||||
var otherBody = contact.OtherBody(entity.Owner);
|
||||
|
||||
// If the blocking body is dynamic then don't ignore it for this.
|
||||
if (otherBody.BodyType != BodyType.Static)
|
||||
continue;
|
||||
|
||||
var otherTransform = PhysicsSystem.GetPhysicsTransform(other);
|
||||
var dotProduct = Vector2.Dot(otherTransform.Position - transform.Position, direction);
|
||||
|
||||
// TODO: This should probably be based on conveyor speed, this is mainly so we don't
|
||||
// go to sleep when conveying and colliding with tables perpendicular to the conveyance direction.
|
||||
if (dotProduct > 1.5f)
|
||||
{
|
||||
direction = Vector2.Zero;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Vector2 Convey(Vector2 direction, float speed, float frameTime, Vector2 itemRelative)
|
||||
private static Vector2 Convey(Vector2 direction, float speed, Vector2 itemRelative)
|
||||
{
|
||||
if (speed == 0 || direction.Length() == 0)
|
||||
if (speed == 0 || direction.LengthSquared() == 0)
|
||||
return Vector2.Zero;
|
||||
|
||||
/*
|
||||
@@ -190,15 +322,15 @@ public abstract class SharedConveyorController : VirtualController
|
||||
if (r.Length() < 0.1)
|
||||
{
|
||||
var velocity = direction * speed;
|
||||
return velocity * frameTime;
|
||||
return velocity;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Give a slight nudge in the direction of the conveyor to prevent
|
||||
// to collidable objects (e.g. crates) on the locker from getting stuck
|
||||
// pushing each other when rounding a corner.
|
||||
var velocity = (r + direction*0.2f).Normalized() * speed;
|
||||
return velocity * frameTime;
|
||||
var velocity = (r + direction).Normalized() * speed;
|
||||
return velocity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,4 +338,55 @@ public abstract class SharedConveyorController : VirtualController
|
||||
{
|
||||
return component.State != ConveyorState.Off && component.Powered;
|
||||
}
|
||||
|
||||
private record struct ConveyorJob : IParallelRobustJob
|
||||
{
|
||||
public int BatchSize => 16;
|
||||
|
||||
public List<(Entity<ConveyedComponent, FixturesComponent, PhysicsComponent, TransformComponent> Entity, Vector2 Direction, bool Result)> Conveyed = new();
|
||||
|
||||
public SharedConveyorController System;
|
||||
|
||||
public bool Prediction;
|
||||
|
||||
public ConveyorJob(SharedConveyorController controller)
|
||||
{
|
||||
System = controller;
|
||||
}
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var convey = Conveyed[index];
|
||||
|
||||
var result = System.TryConvey(
|
||||
(convey.Entity.Owner, convey.Entity.Comp1, convey.Entity.Comp2, convey.Entity.Comp3, convey.Entity.Comp4),
|
||||
Prediction, out var direction);
|
||||
|
||||
Conveyed[index] = (convey.Entity, direction, result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks an entity's contacts to see if it's still being conveyed.
|
||||
/// </summary>
|
||||
private bool IsConveyed(Entity<FixturesComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent.Owner, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
var contacts = PhysicsSystem.GetContacts(ent.Owner);
|
||||
|
||||
while (contacts.MoveNext(out var contact))
|
||||
{
|
||||
if (!contact.IsTouching)
|
||||
continue;
|
||||
|
||||
var other = contact.OtherEnt(ent.Owner);
|
||||
|
||||
if (_conveyorQuery.HasComp(other))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem
|
||||
SubscribeLocalEvent<EmbeddableProjectileComponent, ThrowDoHitEvent>(OnEmbedThrowDoHit);
|
||||
SubscribeLocalEvent<EmbeddableProjectileComponent, ActivateInWorldEvent>(OnEmbedActivate);
|
||||
SubscribeLocalEvent<EmbeddableProjectileComponent, RemoveEmbeddedProjectileEvent>(OnEmbedRemove);
|
||||
SubscribeLocalEvent<EmbeddableProjectileComponent, ComponentShutdown>(OnEmbeddableCompShutdown);
|
||||
|
||||
SubscribeLocalEvent<EmbeddedContainerComponent, EntityTerminatingEvent>(OnEmbeddableTermination);
|
||||
}
|
||||
@@ -75,6 +76,11 @@ public abstract partial class SharedProjectileSystem : EntitySystem
|
||||
_hands.TryPickupAnyHand(args.User, embeddable);
|
||||
}
|
||||
|
||||
private void OnEmbeddableCompShutdown(Entity<EmbeddableProjectileComponent> embeddable, ref ComponentShutdown arg)
|
||||
{
|
||||
EmbedDetach(embeddable, embeddable.Comp);
|
||||
}
|
||||
|
||||
private void OnEmbedThrowDoHit(Entity<EmbeddableProjectileComponent> embeddable, ref ThrowDoHitEvent args)
|
||||
{
|
||||
if (!embeddable.Comp.EmbedOnThrow)
|
||||
@@ -130,16 +136,21 @@ public abstract partial class SharedProjectileSystem : EntitySystem
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
if (component.DeleteOnRemove)
|
||||
{
|
||||
QueueDel(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.EmbeddedIntoUid is not null)
|
||||
{
|
||||
if (TryComp<EmbeddedContainerComponent>(component.EmbeddedIntoUid.Value, out var embeddedContainer))
|
||||
{
|
||||
embeddedContainer.EmbeddedObjects.Remove(uid);
|
||||
Dirty(component.EmbeddedIntoUid.Value, embeddedContainer);
|
||||
if (embeddedContainer.EmbeddedObjects.Count == 0)
|
||||
RemCompDeferred<EmbeddedContainerComponent>(component.EmbeddedIntoUid.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (component.DeleteOnRemove && _net.IsServer)
|
||||
{
|
||||
QueueDel(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
var xform = Transform(uid);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Actions.Events;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction.Events;
|
||||
@@ -122,6 +121,14 @@ public abstract partial class SharedStationAiSystem
|
||||
if (ev.Actor == ev.Target)
|
||||
return;
|
||||
|
||||
// no need to show menu if device is not powered.
|
||||
if (!PowerReceiver.IsPowered(ev.Target))
|
||||
{
|
||||
ShowDeviceNotRespondingPopup(ev.Actor);
|
||||
ev.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp(ev.Actor, out StationAiHeldComponent? aiComp) &&
|
||||
(!TryComp(ev.Target, out StationAiWhitelistComponent? whitelistComponent) ||
|
||||
!ValidateAi((ev.Actor, aiComp))))
|
||||
@@ -150,7 +157,8 @@ public abstract partial class SharedStationAiSystem
|
||||
private void OnTargetVerbs(Entity<StationAiWhitelistComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanComplexInteract
|
||||
|| !HasComp<StationAiHeldComponent>(args.User))
|
||||
|| !HasComp<StationAiHeldComponent>(args.User)
|
||||
|| !args.CanInteract)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -166,13 +174,6 @@ public abstract partial class SharedStationAiSystem
|
||||
Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"),
|
||||
Act = () =>
|
||||
{
|
||||
// no need to show menu if device is not powered.
|
||||
if (!PowerReceiver.IsPowered(ent.Owner))
|
||||
{
|
||||
ShowDeviceNotRespondingPopup(user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOpen)
|
||||
{
|
||||
_uiSystem.CloseUi(ent.Owner, AiUi.Key, user);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Sound.Components;
|
||||
|
||||
@@ -8,10 +10,9 @@ namespace Content.Shared.Sound.Components;
|
||||
/// </summary>
|
||||
public abstract partial class BaseEmitSoundComponent : Component
|
||||
{
|
||||
public static readonly AudioParams DefaultParams = AudioParams.Default.WithVolume(-2f);
|
||||
|
||||
[AutoNetworkedField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
/// <summary>
|
||||
/// The <see cref="SoundSpecifier"/> to play.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public SoundSpecifier? Sound;
|
||||
|
||||
@@ -22,3 +23,15 @@ public abstract partial class BaseEmitSoundComponent : Component
|
||||
[DataField]
|
||||
public bool Positional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state of <see cref="BaseEmitSoundComponent"/>.
|
||||
/// </summary>
|
||||
/// <remarks>This is obviously very cursed, but since the BaseEmitSoundComponent is abstract, we cannot network it.
|
||||
/// AutoGenerateComponentState attribute won't work here, and since everything revolves around inheritance for some fucking reason,
|
||||
/// there's no better way of doing this.</remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public struct EmitSoundComponentState(SoundSpecifier? sound) : IComponentState
|
||||
{
|
||||
public SoundSpecifier? Sound { get; } = sound;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ public sealed partial class EmitSoundOnActivateComponent : BaseEmitSoundComponen
|
||||
/// otherwise this might enable sound spamming, as use-delays are only initiated if the interaction was
|
||||
/// handled.
|
||||
/// </remarks>
|
||||
[DataField("handle")]
|
||||
[DataField]
|
||||
public bool Handle = true;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@ public sealed partial class EmitSoundOnCollideComponent : BaseEmitSoundComponent
|
||||
/// <summary>
|
||||
/// Minimum velocity required for the sound to play.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("minVelocity")]
|
||||
[DataField("minVelocity")]
|
||||
public float MinimumVelocity = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// To avoid sound spam add a cooldown to it.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("nextSound", customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[AutoPausedField]
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan NextSound;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
|
||||
/// Simple sound emitter that emits sound on entity drop
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EmitSoundOnDropComponent : BaseEmitSoundComponent
|
||||
{
|
||||
}
|
||||
public sealed partial class EmitSoundOnDropComponent : BaseEmitSoundComponent;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Sound.Components;
|
||||
@@ -10,6 +9,9 @@ namespace Content.Shared.Sound.Components;
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EmitSoundOnInteractUsingComponent : BaseEmitSoundComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="EntityWhitelist"/> for the entities that can use this item.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public EntityWhitelist Whitelist = new();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
|
||||
/// Simple sound emitter that emits sound on LandEvent
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EmitSoundOnLandComponent : BaseEmitSoundComponent
|
||||
{
|
||||
}
|
||||
public sealed partial class EmitSoundOnLandComponent : BaseEmitSoundComponent;
|
||||
|
||||
@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
|
||||
/// Simple sound emitter that emits sound on entity pickup
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EmitSoundOnPickupComponent : BaseEmitSoundComponent
|
||||
{
|
||||
}
|
||||
public sealed partial class EmitSoundOnPickupComponent : BaseEmitSoundComponent;
|
||||
|
||||
@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
|
||||
/// Simple sound emitter that emits sound on entity spawn.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EmitSoundOnSpawnComponent : BaseEmitSoundComponent
|
||||
{
|
||||
}
|
||||
public sealed partial class EmitSoundOnSpawnComponent : BaseEmitSoundComponent;
|
||||
|
||||
@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
|
||||
/// Simple sound emitter that emits sound on ThrowEvent
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class EmitSoundOnThrowComponent : BaseEmitSoundComponent
|
||||
{
|
||||
}
|
||||
public sealed partial class EmitSoundOnThrowComponent : BaseEmitSoundComponent;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user