Merge branch 'master' into 2020-08-19-firelocks

# Conflicts:
#	Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs
#	Content.Shared/Maps/TurfHelpers.cs
#	SpaceStation14.sln.DotSettings
This commit is contained in:
Víctor Aguilera Puerto
2020-09-06 00:17:48 +02:00
755 changed files with 53590 additions and 34170 deletions

View File

@@ -0,0 +1,45 @@
using Content.Shared.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
namespace Content.Client.GameObjects.Components
{
[UsedImplicitly]
public class AcceptCloningBoundUserInterface : BoundUserInterface
{
public AcceptCloningBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
private AcceptCloningWindow _window;
protected override void Open()
{
base.Open();
_window = new AcceptCloningWindow();
_window.OnClose += Close;
_window.DenyButton.OnPressed += _ => _window.Close();
_window.ConfirmButton.OnPressed += _ =>
{
SendMessage(
new SharedAcceptCloningComponent.UiButtonPressedMessage(
SharedAcceptCloningComponent.UiButton.Accept));
_window.Close();
};
_window.OpenCentered();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Dispose();
}
}
}
}

View File

@@ -0,0 +1,50 @@
#nullable enable
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
namespace Content.Client.GameObjects.Components
{
public sealed class AcceptCloningWindow : SS14Window
{
public readonly Button DenyButton;
public readonly Button ConfirmButton;
public AcceptCloningWindow()
{
Title = Loc.GetString("Cloning Machine");
Contents.AddChild(new VBoxContainer
{
Children =
{
new VBoxContainer
{
Children =
{
(new Label
{
Text = Loc.GetString("You are being cloned! Transfer your soul to the clone body?")
}),
new HBoxContainer
{
Children =
{
(ConfirmButton = new Button
{
Text = Loc.GetString("Yes"),
}),
(DenyButton = new Button
{
Text = Loc.GetString("No"),
})
}
},
}
},
}
});
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Linq;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Input;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.Input;

View File

@@ -0,0 +1,68 @@
using Content.Shared.GameObjects.Components.Atmos;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects.Components.Renderable;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using System;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Atmos
{
[UsedImplicitly]
public class PipeVisualizer : AppearanceVisualizer
{
private RSI _pipeRSI;
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
var rsiString = node.GetNode("pipeRSI").ToString();
var rsiPath = SharedSpriteComponent.TextureRoot / rsiString;
try
{
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resource = resourceCache.GetResource<RSIResource>(rsiPath);
_pipeRSI = resource.RSI;
}
catch (Exception e)
{
Logger.ErrorS("go.pipevisualizer", "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, e);
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent sprite))
{
return;
}
if (!component.TryGetData(PipeVisuals.VisualState, out PipeVisualStateSet pipeVisualStateSet))
{
return;
}
for (var i = 0; i < pipeVisualStateSet.PipeVisualStates.Length; i++)
{
var pipeVisualState = pipeVisualStateSet.PipeVisualStates[i];
var rsiState = "pipe";
rsiState += pipeVisualState.PipeDirection.ToString();
rsiState += ((int) pipeVisualState.ConduitLayer).ToString();
var pipeLayerKey = "pipeLayer" + i.ToString();
sprite.LayerMapReserveBlank(pipeLayerKey);
var currentPipeLayer = sprite.LayerMapGet(pipeLayerKey);
sprite.LayerSetRSI(currentPipeLayer, _pipeRSI);
sprite.LayerSetState(currentPipeLayer, rsiState);
sprite.LayerSetVisible(currentPipeLayer, true);
}
}
}
}

View File

@@ -0,0 +1,85 @@
using Content.Shared.GameObjects.Atmos;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects.Components.Renderable;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using System;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Atmos
{
[UsedImplicitly]
public class PumpVisualizer : AppearanceVisualizer
{
private RSI _pumpRSI;
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
var rsiString = node.GetNode("pumpRSI").ToString();
var rsiPath = SharedSpriteComponent.TextureRoot / rsiString;
try
{
var resourceCache = IoCManager.Resolve<IResourceCache>();
var resource = resourceCache.GetResource<RSIResource>(rsiPath);
_pumpRSI = resource.RSI;
}
catch (Exception e)
{
Logger.ErrorS("go.pumpvisualizer", "Unable to load RSI '{0}'. Trace:\n{1}", rsiPath, e);
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out ISpriteComponent sprite))
{
return;
}
if (!component.TryGetData(PumpVisuals.VisualState, out PumpVisualState pumpVisualState))
{
return;
}
var pumpBaseState = "pump";
pumpBaseState += pumpVisualState.InletDirection.ToString();
pumpBaseState += ((int) pumpVisualState.InletConduitLayer).ToString();
pumpBaseState += pumpVisualState.OutletDirection.ToString();
pumpBaseState += ((int) pumpVisualState.OutletConduitLayer).ToString();
sprite.LayerMapReserveBlank(Layer.PumpBase);
var basePumpLayer = sprite.LayerMapGet(Layer.PumpBase);
sprite.LayerSetRSI(basePumpLayer, _pumpRSI);
sprite.LayerSetState(basePumpLayer, pumpBaseState);
sprite.LayerSetVisible(basePumpLayer, true);
var pumpEnabledAnimationState = "pumpEnabled";
pumpEnabledAnimationState += pumpVisualState.InletDirection.ToString();
pumpEnabledAnimationState += ((int) pumpVisualState.InletConduitLayer).ToString();
pumpEnabledAnimationState += pumpVisualState.OutletDirection.ToString();
pumpEnabledAnimationState += ((int) pumpVisualState.OutletConduitLayer).ToString();
sprite.LayerMapReserveBlank(Layer.PumpEnabled);
var pumpEnabledAnimationLayer = sprite.LayerMapGet(Layer.PumpEnabled);
sprite.LayerSetRSI(pumpEnabledAnimationLayer, _pumpRSI);
sprite.LayerSetState(pumpEnabledAnimationLayer, pumpEnabledAnimationState);
sprite.LayerSetVisible(pumpEnabledAnimationLayer, pumpVisualState.PumpEnabled);
}
private enum Layer
{
PumpBase,
PumpEnabled,
}
}
}

View File

@@ -1,8 +1,10 @@
#nullable enable
using Content.Client.GameObjects.Components.Disposal;
using Content.Client.GameObjects.Components.MedicalScanner;
using Content.Client.Interfaces.GameObjects.Components.Interaction;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Medical;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
@@ -14,14 +16,20 @@ namespace Content.Client.GameObjects.Components.Body
{
[RegisterComponent]
[ComponentReference(typeof(IDamageableComponent))]
[ComponentReference(typeof(IBodyManagerComponent))]
[ComponentReference(typeof(ISharedBodyManagerComponent))]
public class BodyManagerComponent : SharedBodyManagerComponent, IClientDraggable
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public bool ClientCanDropOn(CanDropEventArgs eventArgs)
{
return eventArgs.Target.HasComponent<DisposalUnitComponent>();
if (
eventArgs.Target.HasComponent<DisposalUnitComponent>()||
eventArgs.Target.HasComponent<MedicalScannerComponent>())
{
return true;
}
return false;
}
public bool ClientCanDrag(CanDragEventArgs eventArgs)

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Medical;
using JetBrains.Annotations;
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent;
namespace Content.Client.GameObjects.Components.CloningPod
{
[UsedImplicitly]
public class CloningPodBoundUserInterface : BoundUserInterface
{
public CloningPodBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
private CloningPodWindow _window;
protected override void Open()
{
base.Open();
_window = new CloningPodWindow(new Dictionary<int, string>());
_window.OnClose += Close;
_window.CloneButton.OnPressed += _ =>
{
if (_window.SelectedScan != null)
{
SendMessage(new CloningPodUiButtonPressedMessage(UiButton.Clone, (int) _window.SelectedScan));
}
};
_window.EjectButton.OnPressed += _ =>
{
SendMessage(new CloningPodUiButtonPressedMessage(UiButton.Eject, null));
};
_window.OpenCentered();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
_window.Populate((CloningPodBoundUserInterfaceState) state);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Dispose();
}
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using Content.Shared.GameObjects.Components.Medical;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent;
using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent.CloningPodStatus;
namespace Content.Client.GameObjects.Components.CloningPod
{
public class CloningPodVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if (!component.TryGetData(CloningPodVisuals.Status, out CloningPodStatus status)) return;
sprite.LayerSetState(CloningPodVisualLayers.Machine, StatusToMachineStateId(status));
}
private string StatusToMachineStateId(CloningPodStatus status)
{
//TODO: implement NoMind for if the mind is not yet in the body
//TODO: Find a use for GORE POD
switch (status)
{
case Cloning: return "pod_1";
case NoMind: return "pod_e";
case Gore: return "pod_g";
case Idle: return "pod_0";
default:
throw new ArgumentOutOfRangeException(nameof(status), status, "unknown CloningPodStatus");
}
}
public enum CloningPodVisualLayers
{
Machine,
}
}
}

View File

@@ -0,0 +1,442 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Localization;
using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent;
namespace Content.Client.GameObjects.Components.CloningPod
{
public sealed class CloningPodWindow : SS14Window
{
private Dictionary<int, string> _scanManager;
private readonly VBoxContainer _mainVBox;
private readonly ScanListContainer _scanList;
private readonly LineEdit _searchBar;
private readonly Button _clearButton;
public readonly Button CloneButton;
public readonly Button EjectButton;
private readonly CloningScanButton _measureButton;
private CloningScanButton? _selectedButton;
private Label _progressLabel;
private readonly ProgressBar _cloningProgressBar;
private Label _mindState;
protected override Vector2 ContentsMinimumSize => _mainVBox?.CombinedMinimumSize ?? Vector2.Zero;
private CloningPodBoundUserInterfaceState _lastUpdate = null!;
// List of scans that are visible based on current filter criteria.
private readonly Dictionary<int, string> _filteredScans = new Dictionary<int, string>();
// The indices of the visible scans last time UpdateVisibleScans was ran.
// This is inclusive, so end is the index of the last scan, not right after it.
private (int start, int end) _lastScanIndices;
public int? SelectedScan;
protected override Vector2? CustomSize => (250, 300);
public CloningPodWindow(
Dictionary<int, string> scanManager)
{
_scanManager = scanManager;
Title = Loc.GetString("Cloning Machine");
Contents.AddChild(_mainVBox = new VBoxContainer
{
Children =
{
new HBoxContainer
{
Children =
{
(_searchBar = new LineEdit
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
PlaceHolder = Loc.GetString("Search")
}),
(_clearButton = new Button
{
Disabled = true,
Text = Loc.GetString("Clear"),
})
}
},
new ScrollContainer
{
CustomMinimumSize = new Vector2(200.0f, 0.0f),
SizeFlagsVertical = SizeFlags.FillExpand,
Children =
{
(_scanList = new ScanListContainer())
}
},
new VBoxContainer
{
Children =
{
(CloneButton = new Button
{
Text = Loc.GetString("Clone")
})
}
},
(_measureButton = new CloningScanButton {Visible = false}),
(_cloningProgressBar = new ProgressBar
{
CustomMinimumSize = (200, 20),
SizeFlagsHorizontal = SizeFlags.Fill,
MinValue = 0,
MaxValue = 10,
Page = 0,
Value = 0.5f,
Children =
{
(_progressLabel = new Label())
}
}),
(EjectButton = new Button
{
Text = Loc.GetString("Eject Body")
}),
new HBoxContainer
{
Children =
{
new Label()
{
Text = Loc.GetString("Neural Interface: ")
},
(_mindState = new Label()
{
Text = Loc.GetString("No Activity"),
FontColorOverride = Color.Red
}),
}
}
}
});
_searchBar.OnTextChanged += OnSearchBarTextChanged;
_clearButton.OnPressed += OnClearButtonPressed;
BuildEntityList();
_searchBar.GrabKeyboardFocus();
}
public void Populate(CloningPodBoundUserInterfaceState state)
{
//Ignore useless updates or we can't interact with the UI
//TODO: come up with a better comparision, probably write a comparator because '.Equals' doesn't work
if (_lastUpdate == null || _lastUpdate.MindIdName.Count != state.MindIdName.Count)
{
_scanManager = state.MindIdName;
BuildEntityList();
_lastUpdate = state;
}
var percentage = state.Progress / _cloningProgressBar.MaxValue * 100;
_progressLabel.Text = $"{percentage:0}%";
_cloningProgressBar.Value = state.Progress;
_mindState.Text = Loc.GetString(state.MindPresent ? "Consciousness Detected" : "No Activity");
_mindState.FontColorOverride = state.MindPresent ? Color.Green : Color.Red;
}
private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args)
{
BuildEntityList(args.Text);
_clearButton.Disabled = string.IsNullOrEmpty(args.Text);
}
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
{
_searchBar.Clear();
BuildEntityList("");
}
private void BuildEntityList(string? searchStr = null)
{
_filteredScans.Clear();
_scanList.RemoveAllChildren();
// Reset last scan indices so it automatically updates the entire list.
_lastScanIndices = (0, -1);
_scanList.RemoveAllChildren();
_selectedButton = null;
searchStr = searchStr?.ToLowerInvariant();
foreach (var scan in _scanManager)
{
if (searchStr != null && !_doesScanMatchSearch(scan.Value, searchStr))
{
continue;
}
_filteredScans.Add(scan.Key, scan.Value);
}
//TODO: set up sort
//_filteredScans.Sort((a, b) => string.Compare(a.ToString(), b.ToString(), StringComparison.Ordinal));
_scanList.TotalItemCount = _filteredScans.Count;
}
private void UpdateVisibleScans()
{
// Update visible buttons in the scan list.
// Calculate index of first scan to render based on current scroll.
var height = _measureButton.CombinedMinimumSize.Y + ScanListContainer.Separation;
var offset = -_scanList.Position.Y;
var startIndex = (int) Math.Floor(offset / height);
_scanList.ItemOffset = startIndex;
var (prevStart, prevEnd) = _lastScanIndices;
// Calculate index of final one.
var endIndex = startIndex - 1;
var spaceUsed = -height; // -height instead of 0 because else it cuts off the last button.
while (spaceUsed < _scanList.Parent!.Height)
{
spaceUsed += height;
endIndex += 1;
}
endIndex = Math.Min(endIndex, _filteredScans.Count - 1);
if (endIndex == prevEnd && startIndex == prevStart)
{
// Nothing changed so bye.
return;
}
_lastScanIndices = (startIndex, endIndex);
// Delete buttons at the start of the list that are no longer visible (scrolling down).
for (var i = prevStart; i < startIndex && i <= prevEnd; i++)
{
var control = (CloningScanButton) _scanList.GetChild(0);
DebugTools.Assert(control.Index == i);
_scanList.RemoveChild(control);
}
// Delete buttons at the end of the list that are no longer visible (scrolling up).
for (var i = prevEnd; i > endIndex && i >= prevStart; i--)
{
var control = (CloningScanButton) _scanList.GetChild(_scanList.ChildCount - 1);
DebugTools.Assert(control.Index == i);
_scanList.RemoveChild(control);
}
var array = _filteredScans.ToArray();
// Create buttons at the start of the list that are now visible (scrolling up).
for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--)
{
InsertEntityButton(array[i], true, i);
}
// Create buttons at the end of the list that are now visible (scrolling down).
for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++)
{
InsertEntityButton(array[i], false, i);
}
}
// Create a spawn button and insert it into the start or end of the list.
private void InsertEntityButton(KeyValuePair<int, string> scan, bool insertFirst, int index)
{
var button = new CloningScanButton
{
Scan = scan.Value,
Id = scan.Key,
Index = index // We track this index purely for debugging.
};
button.ActualButton.OnToggled += OnItemButtonToggled;
var entityLabelText = scan.Value;
button.EntityLabel.Text = entityLabelText;
if (scan.Key == SelectedScan)
{
_selectedButton = button;
_selectedButton.ActualButton.Pressed = true;
}
//TODO: replace with body's face
/*var tex = IconComponent.GetScanIcon(scan, resourceCache);
var rect = button.EntityTextureRect;
if (tex != null)
{
rect.Texture = tex.Default;
}
else
{
rect.Dispose();
}
rect.Dispose();
*/
_scanList.AddChild(button);
if (insertFirst)
{
button.SetPositionInParent(0);
}
}
private static bool _doesScanMatchSearch(string scan, string searchStr)
{
return scan.ToLowerInvariant().Contains(searchStr);
}
private void OnItemButtonToggled(BaseButton.ButtonToggledEventArgs args)
{
var item = (CloningScanButton) args.Button.Parent!;
if (_selectedButton == item)
{
_selectedButton = null;
SelectedScan = null;
return;
}
else if (_selectedButton != null)
{
_selectedButton.ActualButton.Pressed = false;
}
_selectedButton = null;
SelectedScan = null;
_selectedButton = item;
SelectedScan = item.Id;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateVisibleScans();
}
private class ScanListContainer : Container
{
// Quick and dirty container to do virtualization of the list.
// Basically, get total item count and offset to put the current buttons at.
// Get a constant minimum height and move the buttons in the list up to match the scrollbar.
private int _totalItemCount;
private int _itemOffset;
public int TotalItemCount
{
get => _totalItemCount;
set
{
_totalItemCount = value;
MinimumSizeChanged();
}
}
public int ItemOffset
{
get => _itemOffset;
set
{
_itemOffset = value;
UpdateLayout();
}
}
public const float Separation = 2;
protected override Vector2 CalculateMinimumSize()
{
if (ChildCount == 0)
{
return Vector2.Zero;
}
var first = GetChild(0);
var (minX, minY) = first.CombinedMinimumSize;
return (minX, minY * TotalItemCount + (TotalItemCount - 1) * Separation);
}
protected override void LayoutUpdateOverride()
{
if (ChildCount == 0)
{
return;
}
var first = GetChild(0);
var height = first.CombinedMinimumSize.Y;
var offset = ItemOffset * height + (ItemOffset - 1) * Separation;
foreach (var child in Children)
{
FitChildInBox(child, UIBox2.FromDimensions(0, offset, Width, height));
offset += Separation + height;
}
}
}
[DebuggerDisplay("cloningbutton {" + nameof(Index) + "}")]
private class CloningScanButton : Control
{
public string Scan { get; set; } = default!;
public int Id { get; set; }
public Button ActualButton { get; private set; }
public Label EntityLabel { get; private set; }
public TextureRect EntityTextureRect { get; private set; }
public int Index { get; set; }
public CloningScanButton()
{
AddChild(ActualButton = new Button
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
ToggleMode = true,
});
AddChild(new HBoxContainer
{
Children =
{
(EntityTextureRect = new TextureRect
{
CustomMinimumSize = (32, 32),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
Stretch = TextureRect.StretchMode.KeepAspectCentered,
CanShrink = true
}),
(EntityLabel = new Label
{
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsHorizontal = SizeFlags.FillExpand,
Text = "",
ClipText = true
})
}
});
}
}
}
}

View File

@@ -115,6 +115,7 @@ namespace Content.Client.GameObjects.Components.Doors
var unlitVisible = true;
var boltedVisible = false;
var weldedVisible = false;
switch (state)
{
case DoorVisualState.Closed:
@@ -145,6 +146,9 @@ namespace Content.Client.GameObjects.Components.Doors
animPlayer.Play(DenyAnimation, AnimationKey);
}
break;
case DoorVisualState.Welded:
weldedVisible = true;
break;
default:
throw new ArgumentOutOfRangeException();
}
@@ -159,6 +163,7 @@ namespace Content.Client.GameObjects.Components.Doors
}
sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, unlitVisible);
sprite.LayerSetVisible(DoorVisualLayers.BaseWelded, weldedVisible);
sprite.LayerSetVisible(DoorVisualLayers.BaseBolted, unlitVisible && boltedVisible);
}
}
@@ -167,6 +172,7 @@ namespace Content.Client.GameObjects.Components.Doors
{
Base,
BaseUnlit,
BaseBolted
BaseWelded,
BaseBolted,
}
}

View File

@@ -0,0 +1,39 @@

using Content.Shared.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using System;
namespace Content.Client.GameObjects.Components
{
[UsedImplicitly]
public class ExpendableLightVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.Deleted)
{
return;
}
if (component.TryGetData(ExpendableLightVisuals.State, out string lightBehaviourID))
{
if (component.Owner.TryGetComponent<LightBehaviourComponent>(out var lightBehaviour))
{
lightBehaviour.StopLightBehaviour();
if (lightBehaviourID != string.Empty)
{
lightBehaviour.StartLightBehaviour(lightBehaviourID);
}
else if (component.Owner.TryGetComponent<PointLightComponent>(out var light))
{
light.Enabled = false;
}
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
using Content.Shared.GameObjects.Components;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
namespace Content.Client.GameObjects.Components
{
public class ExtinguisherCabinetVisualizer : AppearanceVisualizer
{
private string _prefix;
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if (component.TryGetData(ExtinguisherCabinetVisuals.IsOpen, out bool isOpen))
{
if (isOpen)
{
if (component.TryGetData(ExtinguisherCabinetVisuals.ContainsExtinguisher, out bool contains))
{
if (contains)
{
sprite.LayerSetState(0, "extinguisher_full");
}
else
{
sprite.LayerSetState(0, "extinguisher_empty");
}
}
}
else
{
sprite.LayerSetState(0, "extinguisher_closed");
}
}
}
}
}

View File

@@ -0,0 +1,16 @@

using Content.Shared.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Client.GameObjects;
namespace Content.Client.GameObjects.Components.Interactable
{
/// <summary>
/// Component that represents a handheld expendable light which can be activated and eventually dies over time.
/// </summary>
[RegisterComponent]
public class ExpendableLightComponent : SharedExpendableLightComponent
{
}
}

View File

@@ -0,0 +1,539 @@

using System;
using System.Collections.Generic;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Content.Shared.GameObjects.Components;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Interfaces.Serialization;
using Robust.Client.Animations;
using Robust.Shared.Interfaces.GameObjects;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Robust.Client.GameObjects.Components.Animations;
using System.Linq;
namespace Content.Client.GameObjects.Components
{
#region LIGHT_BEHAVIOURS
/// <summary>
/// Base class for all light behaviours to derive from.
/// This AnimationTrack derivative does not rely on keyframes since it often needs to have a randomized duration.
/// </summary>
[Serializable]
public abstract class LightBehaviourAnimationTrack : AnimationTrackProperty, IExposeData
{
[ViewVariables] public string ID { get; set; }
[ViewVariables] public string Property { get; protected set; }
[ViewVariables] public bool IsLooped { get; set; }
[ViewVariables] public bool Enabled { get; set; }
[ViewVariables] public float StartValue { get; set; }
[ViewVariables] public float EndValue { get; set; }
[ViewVariables] public float MinDuration { get; set; }
[ViewVariables] public float MaxDuration { get; set; }
[ViewVariables] public AnimationInterpolationMode InterpolateMode { get; set; }
[ViewVariables] protected float MaxTime { get; set; }
protected PointLightComponent Light = default;
protected IRobustRandom RobustRandom = default;
private float _maxTime = default;
public virtual void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.ID, "id", string.Empty);
serializer.DataField(this, x => x.IsLooped, "isLooped", false);
serializer.DataField(this, x => x.Enabled, "enabled", false);
serializer.DataField(this, x => x.StartValue, "startValue", 0f);
serializer.DataField(this, x => x.EndValue, "endValue", 2f);
serializer.DataField(this, x => x.MinDuration, "minDuration", -1f);
serializer.DataField(this, x => x.MaxDuration, "maxDuration", 2f);
serializer.DataField(this, x => x.Property, "property", "Radius");
serializer.DataField(this, x => x.InterpolateMode, "interpolate", AnimationInterpolationMode.Linear);
}
public void Initialize(PointLightComponent light)
{
Light = light;
RobustRandom = IoCManager.Resolve<IRobustRandom>();
if (Enabled)
{
Light.Enabled = true;
}
OnInitialize();
}
public void UpdatePlaybackValues(Animation owner)
{
Light.Enabled = true;
if (MinDuration > 0)
{
MaxTime = (float) RobustRandom.NextDouble() * (MaxDuration - MinDuration) + MinDuration;
}
else
{
MaxTime = MaxDuration;
}
owner.Length = TimeSpan.FromSeconds(MaxTime);
}
public override (int KeyFrameIndex, float FramePlayingTime) InitPlayback()
{
OnStart();
return (-1, _maxTime);
}
protected void ApplyProperty(object value)
{
if (Property == null)
{
throw new InvalidOperationException("Property parameter is null! Check the prototype!");
}
if (Light is IAnimationProperties properties)
{
properties.SetAnimatableProperty(Property, value);
}
else
{
AnimationHelper.SetAnimatableProperty(Light, Property, value);
}
}
protected override void ApplyProperty(object context, object value)
{
ApplyProperty(value);
}
public virtual void OnInitialize() { }
public virtual void OnStart() { }
}
/// <summary>
/// A light behaviour that alternates between StartValue and EndValue
/// </summary>
public class PulseBehaviour: LightBehaviourAnimationTrack
{
public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback(
object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
{
var playingTime = prevPlayingTime + frameTime;
var interpolateValue = playingTime / MaxTime;
if (Property == "Enabled") // special case for boolean
{
ApplyProperty(interpolateValue < 0.5f? true : false);
return (-1, playingTime);
}
if (interpolateValue < 0.5f)
{
switch (InterpolateMode)
{
case AnimationInterpolationMode.Linear:
ApplyProperty(InterpolateLinear(StartValue, EndValue, interpolateValue * 2f));
break;
case AnimationInterpolationMode.Cubic:
ApplyProperty(InterpolateCubic(EndValue, StartValue, EndValue, StartValue, interpolateValue * 2f));
break;
default:
case AnimationInterpolationMode.Nearest:
ApplyProperty(StartValue);
break;
}
}
else
{
switch (InterpolateMode)
{
case AnimationInterpolationMode.Linear:
ApplyProperty(InterpolateLinear(EndValue, StartValue, (interpolateValue - 0.5f) * 2f));
break;
case AnimationInterpolationMode.Cubic:
ApplyProperty(InterpolateCubic(StartValue, EndValue, StartValue, EndValue, (interpolateValue - 0.5f) * 2f));
break;
default:
case AnimationInterpolationMode.Nearest:
ApplyProperty(EndValue);
break;
}
}
return (-1, playingTime);
}
}
/// <summary>
/// A light behaviour that interpolates from StartValue to EndValue
/// </summary>
public class FadeBehaviour : LightBehaviourAnimationTrack
{
public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback(
object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
{
var playingTime = prevPlayingTime + frameTime;
var interpolateValue = playingTime / MaxTime;
if (Property == "Enabled") // special case for boolean
{
ApplyProperty(interpolateValue < EndValue? true : false);
return (-1, playingTime);
}
switch (InterpolateMode)
{
case AnimationInterpolationMode.Linear:
ApplyProperty(InterpolateLinear(StartValue, EndValue, interpolateValue));
break;
case AnimationInterpolationMode.Cubic:
ApplyProperty(InterpolateCubic(EndValue, StartValue, EndValue, StartValue, interpolateValue));
break;
default:
case AnimationInterpolationMode.Nearest:
ApplyProperty(interpolateValue < 0.5f ? StartValue : EndValue);
break;
}
return (-1, playingTime);
}
}
/// <summary>
/// A light behaviour that interpolates using random values chosen between StartValue and EndValue.
/// </summary>
public class RandomizeBehaviour : LightBehaviourAnimationTrack
{
private object _randomValue1 = default;
private object _randomValue2 = default;
private object _randomValue3 = default;
private object _randomValue4 = default;
public override void OnInitialize()
{
_randomValue2 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble());
_randomValue3 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble());
_randomValue4 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble());
}
public override void OnStart()
{
if (Property == "Enabled") // special case for boolean, we randomize it
{
ApplyProperty(RobustRandom.NextDouble() < 0.5 ? true : false);
return;
}
if (InterpolateMode == AnimationInterpolationMode.Cubic)
{
_randomValue1 = _randomValue2;
_randomValue2 = _randomValue3;
}
_randomValue3 = _randomValue4;
_randomValue4 = InterpolateLinear(StartValue, EndValue, (float) RobustRandom.NextDouble());
}
public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback(
object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
{
var playingTime = prevPlayingTime + frameTime;
var interpolateValue = playingTime / MaxTime;
if (Property == "Enabled")
{
return (-1, playingTime);
}
switch (InterpolateMode)
{
case AnimationInterpolationMode.Linear:
ApplyProperty(InterpolateLinear(_randomValue3, _randomValue4, interpolateValue));
break;
case AnimationInterpolationMode.Cubic:
ApplyProperty(InterpolateCubic(_randomValue1, _randomValue2, _randomValue3, _randomValue4, interpolateValue));
break;
default:
case AnimationInterpolationMode.Nearest:
ApplyProperty(interpolateValue < 0.5f ? _randomValue3 : _randomValue4);
break;
}
return (-1, playingTime);
}
}
/// <summary>
/// A light behaviour that cycles through a list of colors.
/// </summary>
public class ColorCycleBehaviour : LightBehaviourAnimationTrack
{
public List<Color> ColorsToCycle { get; set; }
private int _colorIndex = 0;
public override void OnStart()
{
_colorIndex++;
if (_colorIndex > ColorsToCycle.Count - 1)
{
_colorIndex = 0;
}
}
public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback(
object context, int prevKeyFrameIndex, float prevPlayingTime, float frameTime)
{
var playingTime = prevPlayingTime + frameTime;
var interpolateValue = playingTime / MaxTime;
switch (InterpolateMode)
{
case AnimationInterpolationMode.Linear:
ApplyProperty(InterpolateLinear(ColorsToCycle[(_colorIndex - 1) % ColorsToCycle.Count],
ColorsToCycle[_colorIndex],
interpolateValue));
break;
case AnimationInterpolationMode.Cubic:
ApplyProperty(InterpolateCubic(ColorsToCycle[_colorIndex],
ColorsToCycle[(_colorIndex + 1) % ColorsToCycle.Count],
ColorsToCycle[(_colorIndex + 2) % ColorsToCycle.Count],
ColorsToCycle[(_colorIndex + 3) % ColorsToCycle.Count],
interpolateValue));
break;
default:
case AnimationInterpolationMode.Nearest:
ApplyProperty(ColorsToCycle[_colorIndex]);
break;
}
return (-1, playingTime);
}
public override void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.ID, "id", string.Empty);
serializer.DataField(this, x => x.IsLooped, "isLooped", false);
serializer.DataField(this, x => x.Enabled, "enabled", false);
serializer.DataField(this, x => x.MinDuration, "minDuration", -1f);
serializer.DataField(this, x => x.MaxDuration, "maxDuration", 2f);
serializer.DataField(this, x => x.InterpolateMode, "interpolate", AnimationInterpolationMode.Linear);
ColorsToCycle = serializer.ReadDataField("colors", new List<Color>());
Property = "Color";
if (ColorsToCycle.Count < 2)
{
throw new InvalidOperationException($"{nameof(ColorCycleBehaviour)} has less than 2 colors to cycle");
}
}
}
#endregion
/// <summary>
/// A component which applies a specific behaviour to a PointLightComponent on its owner.
/// </summary>
[RegisterComponent]
public class LightBehaviourComponent : SharedLightBehaviourComponent
{
private const string KeyPrefix = nameof(LightBehaviourComponent);
private class AnimationContainer
{
public AnimationContainer(int key, Animation animation, LightBehaviourAnimationTrack track)
{
Key = key;
Animation = animation;
LightBehaviour = track;
}
public string FullKey => KeyPrefix + Key;
public int Key { get; set; }
public Animation Animation { get; set; }
public LightBehaviourAnimationTrack LightBehaviour { get; set; }
}
[ViewVariables(VVAccess.ReadOnly)]
private List<AnimationContainer> _animations = new List<AnimationContainer>();
private float _originalRadius = default;
private float _originalEnergy = default;
private Angle _originalRotation = default;
private Color _originalColor = default;
private bool _originalEnabled = default;
private PointLightComponent _lightComponent = default;
private AnimationPlayerComponent _animationPlayer = default;
protected override void Startup()
{
base.Startup();
CopyLightSettings();
_animationPlayer = Owner.EnsureComponent<AnimationPlayerComponent>();
_animationPlayer.AnimationCompleted += s => OnAnimationCompleted(s);
foreach (var container in _animations)
{
container.LightBehaviour.Initialize(_lightComponent);
}
// we need to initialize all behaviours before starting any
foreach (var container in _animations)
{
if (container.LightBehaviour.Enabled)
{
StartLightBehaviour(container.LightBehaviour.ID);
}
}
}
private void OnAnimationCompleted(string key)
{
var container = _animations.FirstOrDefault(x => x.FullKey == key);
if (container.LightBehaviour.IsLooped)
{
container.LightBehaviour.UpdatePlaybackValues(container.Animation);
_animationPlayer.Play(container.Animation, container.FullKey);
}
}
/// <summary>
/// If we disable all the light behaviours we want to be able to revert the light to its original state.
/// </summary>
private void CopyLightSettings()
{
if (Owner.TryGetComponent(out _lightComponent))
{
_originalColor = _lightComponent.Color;
_originalEnabled = _lightComponent.Enabled;
_originalEnergy = _lightComponent.Energy;
_originalRadius = _lightComponent.Radius;
_originalRotation = _lightComponent.Rotation;
}
else
{
Logger.Warning($"{Owner.Name} has a {nameof(LightBehaviourComponent)} but it has no {nameof(PointLightComponent)}! Check the prototype!");
}
}
/// <summary>
/// Start animating a light behaviour with the specified ID. If the specified ID is empty, it will start animating all light behaviour entries.
/// If specified light behaviours are already animating, calling this does nothing.
/// Multiple light behaviours can have the same ID.
/// </summary>
public void StartLightBehaviour(string id = "")
{
foreach (var container in _animations)
{
if (container.LightBehaviour.ID == id || id == string.Empty)
{
if (!_animationPlayer.HasRunningAnimation(KeyPrefix + container.Key))
{
container.LightBehaviour.UpdatePlaybackValues(container.Animation);
_animationPlayer.Play(container.Animation, KeyPrefix + container.Key);
}
}
}
}
/// <summary>
/// If any light behaviour with the specified ID is animating, then stop it.
/// If no ID is specified then all light behaviours will be stopped.
/// Multiple light behaviours can have the same ID.
/// </summary>
/// <param name="id"></param>
/// <param name="removeBehaviour">Should the behaviour(s) also be removed permanently?</param>
/// <param name="resetToOriginalSettings">Should the light have its original settings applied?</param>
public void StopLightBehaviour(string id = "", bool removeBehaviour = false, bool resetToOriginalSettings = false)
{
var toRemove = new List<AnimationContainer>();
foreach (var container in _animations)
{
if (container.LightBehaviour.ID == id || id == string.Empty)
{
if (_animationPlayer.HasRunningAnimation(KeyPrefix + container.Key))
{
_animationPlayer.Stop(KeyPrefix + container.Key);
}
if (removeBehaviour)
{
toRemove.Add(container);
}
}
}
foreach (var container in toRemove)
{
_animations.Remove(container);
}
if (resetToOriginalSettings)
{
_lightComponent.Color = _originalColor;
_lightComponent.Enabled = _originalEnabled;
_lightComponent.Energy = _originalEnergy;
_lightComponent.Radius = _originalRadius;
_lightComponent.Rotation = _originalRotation;
}
}
/// <summary>
/// Add a new light behaviour to the component and start it immediately unless otherwise specified.
/// </summary>
public void AddNewLightBehaviour(LightBehaviourAnimationTrack behaviour, bool playImmediately = true)
{
int key = 0;
while (_animations.Any(x => x.Key == key))
{
key++;
}
var animation = new Animation()
{
AnimationTracks = { behaviour }
};
behaviour.Initialize(_lightComponent);
var container = new AnimationContainer(key, animation, behaviour);
_animations.Add(container);
if (playImmediately)
{
StartLightBehaviour(behaviour.ID);
}
}
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
var behaviours = serializer.ReadDataField("behaviours", new List<LightBehaviourAnimationTrack>());
var key = 0;
foreach (LightBehaviourAnimationTrack behaviour in behaviours)
{
var animation = new Animation()
{
AnimationTracks = { behaviour }
};
_animations.Add(new AnimationContainer(key, animation, behaviour));
key++;
}
}
}
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.GameObjects.Components.Medical;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.MedicalScanner
{
[RegisterComponent]
[ComponentReference(typeof(SharedMedicalScannerComponent))]
public class MedicalScannerComponent : SharedMedicalScannerComponent
{
}
}

View File

@@ -12,6 +12,11 @@ namespace Content.Client.GameObjects.Components.MedicalScanner
{
base.OnChangeData(component);
if (component.Owner.Deleted)
{
return;
}
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if (!component.TryGetData(MedicalScannerVisuals.Status, out MedicalScannerStatus status)) return;
sprite.LayerSetState(MedicalScannerVisualLayers.Machine, StatusToMachineStateId(status));

View File

@@ -24,7 +24,7 @@ namespace Content.Client.GameObjects.Components.MedicalScanner
{
(ScanButton = new Button
{
Text = "Scan and Save DNA"
Text = Loc.GetString("Scan and Save DNA")
}),
(_diagnostics = new Label
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.UserInterface;
@@ -100,7 +100,10 @@ namespace Content.Client.GameObjects.Components.Mobs
foreach (var (key, effect) in _status.OrderBy(x => (int) x.Key))
{
var texture = _resourceCache.GetTexture(effect.Icon);
var status = new StatusControl(key, texture);
var status = new StatusControl(key, texture)
{
ToolTip = key.ToString()
};
if (effect.Cooldown.HasValue)
{

View File

@@ -0,0 +1,30 @@
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
public class CriticalState : SharedCriticalState
{
public override void EnterState(IEntity entity)
{
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Critical);
}
EntitySystem.Get<StandingStateSystem>().Down(entity);
}
public override void ExitState(IEntity entity)
{
EntitySystem.Get<StandingStateSystem>().Standing(entity);
}
public override void UpdateState(IEntity entity) { }
}
}

View File

@@ -0,0 +1,41 @@
using Content.Client.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
public class DeadState : SharedDeadState
{
public override void EnterState(IEntity entity)
{
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Dead);
}
EntitySystem.Get<StandingStateSystem>().Down(entity);
if (entity.TryGetComponent(out CollidableComponent collidable))
{
collidable.CanCollide = false;
}
}
public override void ExitState(IEntity entity)
{
EntitySystem.Get<StandingStateSystem>().Standing(entity);
if (entity.TryGetComponent(out CollidableComponent collidable))
{
collidable.CanCollide = true;
}
}
public override void UpdateState(IEntity entity) { }
}
}

View File

@@ -0,0 +1,62 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
[RegisterComponent]
[ComponentReference(typeof(SharedMobStateManagerComponent))]
public class MobStateManagerComponent : SharedMobStateManagerComponent
{
private readonly Dictionary<DamageState, IMobState> _behavior = new Dictionary<DamageState, IMobState>
{
{DamageState.Alive, new NormalState()},
{DamageState.Critical, new CriticalState()},
{DamageState.Dead, new DeadState()}
};
private DamageState _currentDamageState;
protected override IReadOnlyDictionary<DamageState, IMobState> Behavior => _behavior;
public override DamageState CurrentDamageState
{
get => _currentDamageState;
protected set
{
if (_currentDamageState == value)
{
return;
}
if (_currentDamageState != DamageState.Invalid)
{
CurrentMobState.ExitState(Owner);
}
_currentDamageState = value;
CurrentMobState = Behavior[CurrentDamageState];
CurrentMobState.EnterState(Owner);
Dirty();
}
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is MobStateManagerComponentState state))
{
return;
}
_currentDamageState = state.DamageState;
CurrentMobState?.ExitState(Owner);
CurrentMobState = Behavior[CurrentDamageState];
CurrentMobState.EnterState(Owner);
}
}
}

View File

@@ -0,0 +1,25 @@
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State;
using Robust.Client.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.State
{
public class NormalState : SharedNormalState
{
public override void EnterState(IEntity entity)
{
if (entity.TryGetComponent(out AppearanceComponent appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Alive);
}
UpdateState(entity);
}
public override void ExitState(IEntity entity) { }
public override void UpdateState(IEntity entity) { }
}
}

View File

@@ -18,8 +18,7 @@ namespace Content.Client.GameObjects.Components.Observer
private GhostGui _gui;
[ViewVariables(VVAccess.ReadOnly)]
public bool CanReturnToBody { get; private set; } = true;
[ViewVariables(VVAccess.ReadOnly)] public bool CanReturnToBody { get; private set; } = true;
private bool _isAttached;
@@ -51,7 +50,8 @@ namespace Content.Client.GameObjects.Components.Observer
base.Initialize();
if (Owner.TryGetComponent(out SpriteComponent component))
component.Visible = _playerManager.LocalPlayer.ControlledEntity?.HasComponent<GhostComponent>() ?? false;
component.Visible =
_playerManager.LocalPlayer.ControlledEntity?.HasComponent<GhostComponent>() ?? false;
}
public override void HandleMessage(ComponentMessage message, IComponent component)
@@ -98,7 +98,6 @@ namespace Content.Client.GameObjects.Components.Observer
{
_gui?.Update();
}
}
}
}

View File

@@ -0,0 +1,62 @@
using Robust.Client.GameObjects.Components.UserInterface;
using Robust.Shared.GameObjects.Components.UserInterface;
using static Content.Shared.GameObjects.Components.Power.AME.SharedAMEControllerComponent;
namespace Content.Client.GameObjects.Components.Power.AME
{
public class AMEControllerBoundUserInterface : BoundUserInterface
{
private AMEWindow _window;
public AMEControllerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new AMEWindow();
_window.OnClose += Close;
_window.OpenCentered();
_window.EjectButton.OnPressed += _ => ButtonPressed(UiButton.Eject);
_window.ToggleInjection.OnPressed += _ => ButtonPressed(UiButton.ToggleInjection);
_window.IncreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.IncreaseFuel);
_window.DecreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.DecreaseFuel);
_window.RefreshPartsButton.OnPressed += _ => ButtonPressed(UiButton.RefreshParts);
}
/// <summary>
/// Update the ui each time new state data is sent from the server.
/// </summary>
/// <param name="state">
/// Data of the <see cref="SharedReagentDispenserComponent"/> that this ui represents.
/// Sent from the server.
/// </param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
var castState = (AMEControllerBoundUserInterfaceState) state;
_window?.UpdateState(castState); //Update window state
}
private void ButtonPressed(UiButton button, int dispenseIndex = -1)
{
SendMessage(new UiButtonPressedMessage(button));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window.Dispose();
}
}
}
}

View File

@@ -0,0 +1,57 @@
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using System;
using System.Collections.Generic;
using System.Text;
using static Content.Shared.GameObjects.Components.Power.AME.SharedAMEControllerComponent;
namespace Content.Client.GameObjects.Components.Power.AME
{
public class AMEControllerVisualizer : AppearanceVisualizer
{
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
var sprite = entity.GetComponent<ISpriteComponent>();
sprite.LayerMapSet(Layers.Display, sprite.AddLayerState("control_on"));
sprite.LayerSetVisible(Layers.Display, false);
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if (component.TryGetData<string>(AMEControllerVisuals.DisplayState, out var state))
{
switch (state)
{
case "on":
sprite.LayerSetState(Layers.Display, "control_on");
sprite.LayerSetVisible(Layers.Display, true);
break;
case "critical":
sprite.LayerSetState(Layers.Display, "control_critical");
sprite.LayerSetVisible(Layers.Display, true);
break;
case "fuck":
sprite.LayerSetState(Layers.Display, "control_fuck");
sprite.LayerSetVisible(Layers.Display, true);
break;
case "off":
sprite.LayerSetVisible(Layers.Display, false);
break;
default:
sprite.LayerSetVisible(Layers.Display, false);
break;
}
}
}
enum Layers
{
Display,
}
}
}

View File

@@ -0,0 +1,63 @@
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Interfaces.GameObjects;
using System;
using System.Collections.Generic;
using System.Text;
using static Content.Shared.GameObjects.Components.Power.AME.SharedAMEShieldComponent;
namespace Content.Client.GameObjects.Components.Power.AME
{
public class AMEVisualizer : AppearanceVisualizer
{
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
var sprite = entity.GetComponent<ISpriteComponent>();
sprite.LayerMapSet(Layers.Core, sprite.AddLayerState("core"));
sprite.LayerSetVisible(Layers.Core, false);
sprite.LayerMapSet(Layers.CoreState, sprite.AddLayerState("core_weak"));
sprite.LayerSetVisible(Layers.CoreState, false);
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = component.Owner.GetComponent<ISpriteComponent>();
if (component.TryGetData<string>(AMEShieldVisuals.Core, out var core))
{
if (core == "isCore")
{
sprite.LayerSetState(Layers.Core, "core");
sprite.LayerSetVisible(Layers.Core, true);
}
else
{
sprite.LayerSetVisible(Layers.Core, false);
}
}
if (component.TryGetData<string>(AMEShieldVisuals.CoreState, out var coreState))
switch (coreState)
{
case "weak":
sprite.LayerSetState(Layers.CoreState, "core_weak");
sprite.LayerSetVisible(Layers.CoreState, true);
break;
case "strong":
sprite.LayerSetState(Layers.CoreState, "core_strong");
sprite.LayerSetVisible(Layers.CoreState, true);
break;
case "off":
sprite.LayerSetVisible(Layers.CoreState, false);
break;
}
}
}
enum Layers
{
Core,
CoreState,
}
}

View File

@@ -0,0 +1,162 @@
using Content.Client.UserInterface.Stylesheets;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects.Components.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using System;
using System.Collections.Generic;
using System.Text;
using static Content.Shared.GameObjects.Components.Power.AME.SharedAMEControllerComponent;
namespace Content.Client.GameObjects.Components.Power.AME
{
public class AMEWindow : SS14Window
{
public Label InjectionStatus { get; set; }
public Button EjectButton { get; set; }
public Button ToggleInjection { get; set; }
public Button IncreaseFuelButton { get; set; }
public Button DecreaseFuelButton { get; set; }
public Button RefreshPartsButton { get; set; }
public ProgressBar FuelMeter { get; set; }
public Label FuelAmount { get; set; }
public Label InjectionAmount { get; set; }
public Label CoreCount { get; set; }
public AMEWindow()
{
IoCManager.InjectDependencies(this);
Title = "Antimatter Control Unit";
Contents.AddChild(new VBoxContainer
{
Children =
{
new HBoxContainer
{
Children =
{
new Label {Text = Loc.GetString("Engine Status") + ": "},
(InjectionStatus = new Label {Text = "Not Injecting"})
}
},
new HBoxContainer
{
Children =
{
(ToggleInjection = new Button {Text = "Toggle Injection", StyleClasses = {StyleBase.ButtonOpenBoth}, Disabled = true}),
}
},
new HBoxContainer
{
Children =
{
new Label {Text = Loc.GetString("Fuel Status") + ": "},
(FuelAmount = new Label {Text = "No fuel inserted"})
}
},
new HBoxContainer
{
Children =
{
(EjectButton = new Button {Text = "Eject", StyleClasses = {StyleBase.ButtonOpenBoth}, Disabled = true}),
}
},
new HBoxContainer
{
Children =
{
new Label {Text = Loc.GetString("Injection amount") + ": "},
(InjectionAmount = new Label {Text = "0"})
}
},
new HBoxContainer
{
Children =
{
(IncreaseFuelButton = new Button {Text = "Increase", StyleClasses = {StyleBase.ButtonOpenRight}}),
(DecreaseFuelButton = new Button {Text = "Decrease", StyleClasses = {StyleBase.ButtonOpenLeft}}),
}
},
new HBoxContainer
{
Children =
{
(RefreshPartsButton = new Button {Text = "Refresh Parts", StyleClasses = {StyleBase.ButtonOpenBoth }, Disabled = true }),
new Label { Text = Loc.GetString("Core count") + ": "},
(CoreCount = new Label { Text = "0"}),
}
}
}
});
}
/// <summary>
/// This searches recursively through all the children of "parent"
/// and sets the Disabled value of any buttons found to "val"
/// </summary>
/// <param name="parent">The control which childrens get searched</param>
/// <param name="val">The value to which disabled gets set</param>
private void SetButtonDisabledRecursive(Control parent, bool val)
{
foreach (var child in parent.Children)
{
if (child is Button but)
{
but.Disabled = val;
continue;
}
if (child.Children != null)
{
SetButtonDisabledRecursive(child, val);
}
}
}
/// <summary>
/// Update the UI state when new state data is received from the server.
/// </summary>
/// <param name="state">State data sent by the server.</param>
public void UpdateState(BoundUserInterfaceState state)
{
var castState = (AMEControllerBoundUserInterfaceState) state;
// Disable all buttons if not powered
if (Contents.Children != null)
{
SetButtonDisabledRecursive(Contents, !castState.HasPower);
EjectButton.Disabled = false;
}
if(!castState.HasFuelJar)
{
EjectButton.Disabled = true;
ToggleInjection.Disabled = true;
FuelAmount.Text = Loc.GetString("No fuel inserted");
}
else
{
EjectButton.Disabled = false;
ToggleInjection.Disabled = false;
FuelAmount.Text = $"{castState.FuelAmount}";
}
if(!castState.IsMaster)
{
ToggleInjection.Disabled = true;
}
RefreshPartsButton.Disabled = castState.Injecting;
CoreCount.Text = $"{castState.CoreCount}";
InjectionAmount.Text = $"{castState.InjectionAmount}";
}
}
}