mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-16 21:17:39 +03:00
# Description I am trying to port over the AI turrets being implemented into wizden made by chromiumboy. It looks fantastic and would like to port this now and work on any issues that might show. --- # Original PRs https://github.com/space-wizards/space-station-14/issues/35223 https://github.com/space-wizards/space-station-14/pull/35025 https://github.com/space-wizards/space-station-14/pull/35031 https://github.com/space-wizards/space-station-14/pull/35058 https://github.com/space-wizards/space-station-14/pull/35123 https://github.com/space-wizards/space-station-14/pull/35149 https://github.com/space-wizards/space-station-14/pull/35235 https://github.com/space-wizards/space-station-14/pull/35236 --- # TODO - [x] Port all related PRs to EE. - [x] Patch any bugs with turrets or potential issues. - [x] Cleanup my shitcode or changes. --- # Changelog 🆑 - add: Added recharging sentry turrets, one is AI-based or the other is Sec can make. - add: The sentry turrets can be made after researching in T3 arsenal. The boards are made in the sec fab. - add: New ID permissions for borgs and minibots for higher turret options. - tweak: Turrets stop shooting after someone goes crit. --------- Co-authored-by: Nathaniel Adams <60526456+Nathaniel-Adams@users.noreply.github.com> (cherry picked from commit 209d0537401cbda448a03e910cca9a898c9d566f)
502 lines
17 KiB
C#
502 lines
17 KiB
C#
using Content.Client.Resources;
|
|
using Content.Client.Stylesheets;
|
|
using Content.Client.UserInterface.Controls;
|
|
using Content.Shared.Access;
|
|
using Content.Shared.Access.Systems;
|
|
using Content.Shared.TurretController;
|
|
using Content.Shared.Turrets;
|
|
using Robust.Client.AutoGenerated;
|
|
using Robust.Client.Player;
|
|
using Robust.Client.ResourceManagement;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Client.UserInterface.CustomControls;
|
|
using Robust.Client.UserInterface.XAML;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
|
|
namespace Content.Client.TurretController;
|
|
|
|
[GenerateTypedNameReferences]
|
|
public sealed partial class TurretControllerWindow : BaseWindow
|
|
{
|
|
[Dependency] private IEntityManager _entManager = default!;
|
|
[Dependency] private IPrototypeManager _protoManager = default!;
|
|
[Dependency] private IPlayerManager _playerManager = default!;
|
|
|
|
private readonly IResourceCache _cache;
|
|
private readonly AccessReaderSystem _accessReaderSystem;
|
|
|
|
private EntityUid? _owner;
|
|
private int _tabIndex = 0;
|
|
|
|
// Button groups
|
|
private readonly ButtonGroup _armamentButtons = new();
|
|
private readonly ButtonGroup _accessGroupsButtons = new();
|
|
|
|
// Temp values
|
|
private List<CheckBox> _checkBoxes = new();
|
|
private HashSet<AccessLevelPrototype> _accessLevelsForTab = new();
|
|
private List<AccessLevelEntry> _accessLevelEntries = new();
|
|
|
|
// Events
|
|
private event Action<int>? OnAccessGroupChangedEvent;
|
|
|
|
public event Action<HashSet<ProtoId<AccessLevelPrototype>>, bool>? OnAccessLevelsChangedEvent;
|
|
public event Action<int>? OnArmamentSettingChangedEvent;
|
|
|
|
// Colors
|
|
private Color[] _themeColors = [Color.FromHex("#33e633"), Color.FromHex("#dfb827"), Color.FromHex("#da2a2a")];
|
|
|
|
public TurretControllerWindow()
|
|
{
|
|
RobustXamlLoader.Load(this);
|
|
IoCManager.InjectDependencies(this);
|
|
|
|
_cache = IoCManager.Resolve<IResourceCache>();
|
|
_accessReaderSystem = _entManager.System<AccessReaderSystem>();
|
|
|
|
CloseButton.OnPressed += _ => Close();
|
|
XamlChildren = ContentsContainer.Children;
|
|
|
|
OnAccessGroupChangedEvent += OnAccessGroupChanged;
|
|
|
|
var smallFont = _cache.NotoStack(size: 8);
|
|
Footer.FontOverride = smallFont;
|
|
}
|
|
|
|
private void Initialize()
|
|
{
|
|
if (_owner == null)
|
|
return;
|
|
|
|
// Set up armament buttons
|
|
SafeButton.OnToggled += args => OnArmamentButtonPressed(SafeButton, -1);
|
|
StunButton.OnToggled += args => OnArmamentButtonPressed(StunButton, 0);
|
|
LethalButton.OnToggled += args => OnArmamentButtonPressed(LethalButton, 1);
|
|
|
|
SafeButton.Group = _armamentButtons;
|
|
StunButton.Group = _armamentButtons;
|
|
LethalButton.Group = _armamentButtons;
|
|
|
|
SafeButton.Label.AddStyleClass("ConsoleText");
|
|
StunButton.Label.AddStyleClass("ConsoleText");
|
|
LethalButton.Label.AddStyleClass("ConsoleText");
|
|
|
|
// Refresh UI
|
|
RefreshLinkedTurrets(new());
|
|
|
|
if (_entManager.TryGetComponent<DeployableTurretControllerComponent>(_owner, out var turretController))
|
|
UpdateTheme(turretController.ArmamentState);
|
|
|
|
if (_entManager.TryGetComponent<TurretTargetSettingsComponent>(_owner, out var turretTargetSettings))
|
|
RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
|
|
}
|
|
|
|
private void OnArmamentButtonPressed(Button pressedButton, int index)
|
|
{
|
|
UpdateTheme(index);
|
|
OnArmamentSettingChangedEvent?.Invoke(index);
|
|
}
|
|
|
|
private void UpdateTheme(int index)
|
|
{
|
|
switch (index)
|
|
{
|
|
case -1:
|
|
SafeButton.Pressed = true;
|
|
break;
|
|
case 0:
|
|
StunButton.Pressed = true;
|
|
break;
|
|
case 1:
|
|
LethalButton.Pressed = true;
|
|
break;
|
|
}
|
|
|
|
var canInteract = IsLocalPlayerAllowedToInteract();
|
|
|
|
SafeButton.Disabled = !SafeButton.Pressed && !canInteract;
|
|
StunButton.Disabled = !StunButton.Pressed && !canInteract;
|
|
LethalButton.Disabled = !LethalButton.Pressed && !canInteract;
|
|
|
|
var shiftedIndex = index + 1;
|
|
|
|
if (shiftedIndex >= 0 && shiftedIndex < _themeColors.Length)
|
|
ContentsContainer.Modulate = _themeColors[shiftedIndex];
|
|
}
|
|
|
|
public void SetOwner(EntityUid owner)
|
|
{
|
|
_owner = owner;
|
|
|
|
Initialize();
|
|
}
|
|
|
|
public void UpdateState(DeployableTurretControllerBoundInterfaceState state)
|
|
{
|
|
if (_entManager.TryGetComponent<DeployableTurretControllerComponent>(_owner, out var turretController))
|
|
UpdateTheme(turretController.ArmamentState);
|
|
|
|
if (_entManager.TryGetComponent<TurretTargetSettingsComponent>(_owner, out var turretTargetSettings))
|
|
RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
|
|
|
|
RefreshLinkedTurrets(state.TurretStates);
|
|
}
|
|
|
|
public void UpdateMessage(DeployableTurretControllerBoundInterfaceMessage message)
|
|
{
|
|
RefreshLinkedTurrets(message.TurretStates);
|
|
}
|
|
|
|
public void RefreshLinkedTurrets(List<(string, string)> turretStates)
|
|
{
|
|
var turretCount = turretStates.Count;
|
|
var hasTurrets = turretCount > 0;
|
|
|
|
NoLinkedTurretsText.Visible = !hasTurrets;
|
|
LinkedTurretsContainer.Visible = hasTurrets;
|
|
|
|
LinkedTurretsContainer.RemoveAllChildren();
|
|
|
|
foreach (var turretState in turretStates)
|
|
{
|
|
var box = new BoxContainer()
|
|
{
|
|
HorizontalExpand = true,
|
|
};
|
|
|
|
var label = new Label()
|
|
{
|
|
Text = Loc.GetString("turret-controls-window-turret-status", ("device", turretState.Item1), ("status", Loc.GetString(turretState.Item2))),
|
|
HorizontalAlignment = HAlignment.Left,
|
|
Margin = new Thickness(10f, 0f, 10f, 0f),
|
|
HorizontalExpand = true,
|
|
SetHeight = 20f,
|
|
};
|
|
|
|
label.AddStyleClass("ConsoleText");
|
|
|
|
box.AddChild(label);
|
|
LinkedTurretsContainer.AddChild(box);
|
|
}
|
|
|
|
TurretStatusHeader.Text = Loc.GetString("turret-controls-window-turret-status-label", ("count", turretCount));
|
|
}
|
|
|
|
public void RefreshAccessControls(HashSet<ProtoId<AccessLevelPrototype>> exemptAccessLevels)
|
|
{
|
|
if (_owner == null)
|
|
return;
|
|
|
|
if (!_entManager.TryGetComponent<DeployableTurretControllerComponent>(_owner, out var turretControls))
|
|
return;
|
|
|
|
var canInteract = IsLocalPlayerAllowedToInteract();
|
|
|
|
// Create a list of known access groups with which to populate the UI
|
|
var groupedAccessLevels = new Dictionary<AccessGroupPrototype, HashSet<AccessLevelPrototype>>();
|
|
|
|
foreach (var accessGroup in turretControls.AccessGroups)
|
|
{
|
|
if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
|
|
continue;
|
|
|
|
groupedAccessLevels.Add(accessGroupProto, new());
|
|
}
|
|
|
|
// Ensure that the 'general' access group is added to handle
|
|
// misc. access levels that aren't associated with any group
|
|
if (_protoManager.TryIndex<AccessGroupPrototype>("General", out var generalAccessProto))
|
|
groupedAccessLevels.TryAdd(generalAccessProto, new());
|
|
|
|
// Assign known access levels with their associated groups
|
|
foreach (var accessLevel in turretControls.AccessLevels)
|
|
{
|
|
if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
|
|
continue;
|
|
|
|
IEnumerable<AccessGroupPrototype> associatedGroups =
|
|
groupedAccessLevels.Keys.Where(x => x.Tags.Contains(accessLevelProto.ID) == true);
|
|
|
|
if (!associatedGroups.Any() && generalAccessProto != null)
|
|
groupedAccessLevels[generalAccessProto].Add(accessLevelProto);
|
|
|
|
else
|
|
{
|
|
foreach (var group in associatedGroups)
|
|
groupedAccessLevels[group].Add(accessLevelProto);
|
|
}
|
|
}
|
|
|
|
// Remove access groups that have no assigned access levels
|
|
foreach (var (group, accessLevels) in groupedAccessLevels)
|
|
{
|
|
if (accessLevels.Count == 0)
|
|
groupedAccessLevels.Remove(group);
|
|
}
|
|
|
|
// Did something go wrong...?
|
|
if (groupedAccessLevels.Count == 0)
|
|
{
|
|
AccessGroupList.DisposeAllChildren();
|
|
AccessLevelGrid.DisposeAllChildren();
|
|
|
|
return;
|
|
}
|
|
|
|
// Adjust the current tab index so it remains in range
|
|
if (_tabIndex >= groupedAccessLevels.Count)
|
|
_tabIndex = groupedAccessLevels.Count - 1;
|
|
|
|
// Reorder the access groups alphabetically
|
|
var orderedAccessGroups = groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
|
|
|
|
// Remove excess group access buttons from the UI
|
|
while (AccessGroupList.ChildCount > orderedAccessGroups.Count)
|
|
AccessGroupList.RemoveChild(orderedAccessGroups.Count - 1);
|
|
|
|
// Add missing group access buttons to the UI
|
|
while (AccessGroupList.ChildCount < orderedAccessGroups.Count)
|
|
{
|
|
var monotoneButton = new MonotoneButton
|
|
{
|
|
ToggleMode = true,
|
|
};
|
|
|
|
AccessGroupList.AddChild(monotoneButton);
|
|
|
|
// Add button styling
|
|
monotoneButton.Label.AddStyleClass("ConsoleText");
|
|
monotoneButton.Label.HorizontalAlignment = HAlignment.Left;
|
|
|
|
monotoneButton.Group = _accessGroupsButtons;
|
|
|
|
var childIndex = AccessGroupList.ChildCount - 1;
|
|
|
|
if (orderedAccessGroups.Count > 1)
|
|
{
|
|
if (childIndex == 0)
|
|
monotoneButton.Shape = MonotoneButtonShape.OpenLeft;
|
|
|
|
else if (orderedAccessGroups.Count > 1 && childIndex == (orderedAccessGroups.Count - 1))
|
|
monotoneButton.Shape = MonotoneButtonShape.OpenRight;
|
|
|
|
else
|
|
monotoneButton.Shape = MonotoneButtonShape.OpenBoth;
|
|
}
|
|
|
|
// Add button events
|
|
monotoneButton.OnPressed += _ =>
|
|
{
|
|
OnAccessGroupChangedEvent?.Invoke(monotoneButton.GetPositionInParent());
|
|
};
|
|
}
|
|
|
|
// Update the group access buttons
|
|
for (int i = 0; i < orderedAccessGroups.Count; i++)
|
|
{
|
|
if (AccessGroupList.GetChild(i) is not Button { } accessGroupButton)
|
|
continue;
|
|
|
|
var accessGroup = orderedAccessGroups[i];
|
|
var prefix = groupedAccessLevels[accessGroup].Any(x => exemptAccessLevels.Contains(x)) ? "»" : " ";
|
|
|
|
accessGroupButton.Text = Loc.GetString("turret-controls-window-access-group-label",
|
|
("prefix", prefix), ("label", accessGroup.GetAccessGroupName()));
|
|
|
|
accessGroupButton.Pressed = _tabIndex == orderedAccessGroups.IndexOf(accessGroup);
|
|
}
|
|
|
|
// Get the access levels associated with the current tab
|
|
_accessLevelsForTab = groupedAccessLevels[orderedAccessGroups[_tabIndex]];
|
|
_accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToHashSet();
|
|
|
|
// Remove excess access level buttons from the UI
|
|
// Note: if _accessLevelsForTab is length 'n', AccessLevelGrid should have 'n + 1' children at the end
|
|
while (AccessLevelGrid.ChildCount > (_accessLevelsForTab.Count + 1))
|
|
{
|
|
var index = AccessLevelGrid.ChildCount - 1;
|
|
|
|
if (AccessLevelGrid.GetChild(AccessLevelGrid.ChildCount - 1) is AccessLevelEntry { } accessLevelEntry)
|
|
_accessLevelEntries.Remove(accessLevelEntry);
|
|
|
|
AccessLevelGrid.RemoveChild(index);
|
|
}
|
|
|
|
// Add an 'all' checkbox as the first child of the list if it hasn't been initalized yet
|
|
// Toggling this checkbox on will mark all other boxes below it on/off
|
|
if (AccessLevelGrid.ChildCount == 0)
|
|
{
|
|
var checkBox = new MonotoneCheckBox
|
|
{
|
|
Text = Loc.GetString("turret-controls-window-all-checkbox"),
|
|
Margin = new Thickness(0, 0, 0, 3),
|
|
ToggleMode = true,
|
|
ReservesSpace = false,
|
|
};
|
|
|
|
AccessLevelGrid.AddChild(checkBox);
|
|
|
|
// Add checkbox styling
|
|
checkBox.Label.AddStyleClass("ConsoleText");
|
|
|
|
// Add checkbox events
|
|
checkBox.OnPressed += args =>
|
|
{
|
|
SetCheckBoxPressedState(_checkBoxes, checkBox.Pressed);
|
|
|
|
var accessLevels = new HashSet<ProtoId<AccessLevelPrototype>>();
|
|
|
|
foreach (var accessLevel in _accessLevelsForTab)
|
|
accessLevels.Add(accessLevel);
|
|
|
|
OnAccessLevelsChangedEvent?.Invoke(accessLevels, checkBox.Pressed);
|
|
};
|
|
}
|
|
|
|
// Hide the 'all' checkbox if the tab has only one access level
|
|
var allCheckBoxVisible = _accessLevelsForTab.Count > 1;
|
|
|
|
// Did something go wrong...?
|
|
if (AccessLevelGrid.GetChild(0) is not CheckBox { } allCheckBox)
|
|
return;
|
|
|
|
allCheckBox.Visible = allCheckBoxVisible;
|
|
allCheckBox.Disabled = !canInteract;
|
|
|
|
// Add any remaining missing access level buttons to the UI
|
|
while (AccessLevelGrid.ChildCount < (_accessLevelsForTab.Count + 1))
|
|
{
|
|
var accessLevelEntry = new AccessLevelEntry();
|
|
AccessLevelGrid.AddChild(accessLevelEntry);
|
|
|
|
_accessLevelEntries.Add(accessLevelEntry);
|
|
|
|
// Add checkbox events
|
|
accessLevelEntry.CheckBox.OnPressed += args =>
|
|
{
|
|
// If the checkbox and its siblings are checked, check the 'all' checkbox too
|
|
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => (CheckBox)x.CheckBox));
|
|
|
|
OnAccessLevelsChangedEvent?.Invoke
|
|
(new HashSet<ProtoId<AccessLevelPrototype>>() { accessLevelEntry.AccessLevel }, accessLevelEntry.CheckBox.Pressed);
|
|
};
|
|
}
|
|
|
|
// Update the access levels buttons' appearance
|
|
for (int i = 0; i < _accessLevelEntries.Count; i++)
|
|
{
|
|
var accessLevel = _accessLevelsForTab.ElementAt(i);
|
|
var accessLevelEntry = _accessLevelEntries[i];
|
|
|
|
accessLevelEntry.AccessLevel = accessLevel;
|
|
accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName();
|
|
accessLevelEntry.CheckBox.Pressed = exemptAccessLevels.Contains(accessLevel);
|
|
|
|
var isEndOfList = i == (_accessLevelEntries.Count - 1);
|
|
|
|
var lines = new List<(Vector2, Vector2)>()
|
|
{
|
|
(new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)),
|
|
(new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)),
|
|
};
|
|
|
|
accessLevelEntry.UpdateCheckBoxLink(lines);
|
|
accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible;
|
|
accessLevelEntry.CheckBoxLink.Modulate = !canInteract ? Color.Gray : Color.White;
|
|
|
|
accessLevelEntry.CheckBox.Disabled = !canInteract;
|
|
}
|
|
|
|
// Press the 'all' checkbox if all others are pressed
|
|
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
|
|
}
|
|
|
|
|
|
private bool AreAllCheckBoxesPressed(IEnumerable<CheckBox> checkBoxes)
|
|
{
|
|
foreach (var checkBox in checkBoxes)
|
|
{
|
|
if (!checkBox.Pressed)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void SetCheckBoxPressedState(IEnumerable<CheckBox> checkBoxes, bool pressed)
|
|
{
|
|
foreach (var checkBox in checkBoxes)
|
|
checkBox.Pressed = pressed;
|
|
}
|
|
|
|
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
|
|
{
|
|
return DragMode.Move;
|
|
}
|
|
|
|
private void OnAccessGroupChanged(int newTabIndex)
|
|
{
|
|
if (newTabIndex == _tabIndex)
|
|
return;
|
|
|
|
_tabIndex = newTabIndex;
|
|
|
|
if (_entManager.TryGetComponent<TurretTargetSettingsComponent>(_owner, out var turretTargetSettings))
|
|
RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
|
|
}
|
|
|
|
private bool IsLocalPlayerAllowedToInteract()
|
|
{
|
|
if (_owner == null || _playerManager.LocalSession?.AttachedEntity == null)
|
|
return false;
|
|
|
|
return _accessReaderSystem.IsAllowed(_playerManager.LocalSession.AttachedEntity.Value, _owner.Value);
|
|
}
|
|
|
|
private sealed class AccessLevelEntry : BoxContainer
|
|
{
|
|
public ProtoId<AccessLevelPrototype> AccessLevel = default!;
|
|
public MonotoneCheckBox CheckBox;
|
|
public LineRenderer CheckBoxLink;
|
|
|
|
public AccessLevelEntry()
|
|
{
|
|
HorizontalExpand = true;
|
|
|
|
var lines = new List<(Vector2, Vector2)>()
|
|
{
|
|
(new Vector2(0,0), new Vector2(0,0)),
|
|
(new Vector2(0,0), new Vector2(0,0))
|
|
};
|
|
|
|
CheckBoxLink = new LineRenderer(lines)
|
|
{
|
|
SetWidth = 22,
|
|
VerticalExpand = true,
|
|
Margin = new Thickness(0, -1),
|
|
ReservesSpace = false,
|
|
};
|
|
|
|
AddChild(CheckBoxLink);
|
|
|
|
CheckBox = new MonotoneCheckBox
|
|
{
|
|
ToggleMode = true,
|
|
Margin = new Thickness(0f, 0f, 0f, 3f),
|
|
};
|
|
|
|
AddChild(CheckBox);
|
|
|
|
CheckBox.Label.AddStyleClass("ConsoleText");
|
|
}
|
|
|
|
public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines)
|
|
{
|
|
CheckBoxLink.Lines = lines;
|
|
}
|
|
}
|
|
}
|