Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Content.Shared.Imperial.XxRaay.Components;
using Robust.Client.UserInterface;

namespace Content.Client.Imperial.XxRaay.UI;

public sealed class AnimatronicControllerBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private AnimatronicControllerWindow? _window;

public AnimatronicControllerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}

protected override void Open()
{
base.Open();

_window = this.CreateWindow<AnimatronicControllerWindow>();

_window.OnAnimatronicWaypointSelected += (animEntity, wpEntity) =>
{
SendMessage(new SetAnimatronicTargetEvent(animEntity, wpEntity));
};

_window.OnAnimatronicClearTarget += (animEntity) =>
{
SendMessage(new SetAnimatronicTargetEvent(animEntity, true));
};

_window.OnAnimatronicObserving += (animEntity) =>
{
SendMessage(new SetAnimatronicObservingEvent(animEntity));
};

SendMessage(new RequestAnimDataEvent());
}

protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);

if (state is AnimDataStateEvent animState)
{
_window?.UpdateState(animState);
}
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Dispose();
}
}
}

15 changes: 15 additions & 0 deletions Content.Client/Imperial/XxRaay/UI/AnimatronicControllerWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<i:BaseImperialWindow
xmlns="https://spacestation14.io"
xmlns:i="clr-namespace:Content.Client.Imperial.UI.Windows.BaseImperialWindow"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ac="clr-namespace:Content.Client.Imperial.XxRaay.UI"
Comment on lines +4 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Неиспользуемые пространства имён controls и ac.

Оба xmlns-объявления (xmlns:controls и xmlns:ac) не используются в XAML. Их можно удалить.

♻️ Предлагаемое исправление
 <i:BaseImperialWindow
     xmlns="https://spacestation14.io"
     xmlns:i="clr-namespace:Content.Client.Imperial.UI.Windows.BaseImperialWindow"
-    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
-    xmlns:ac="clr-namespace:Content.Client.Imperial.XxRaay.UI"
     HeaderText="{Loc animatronic-controller-window-title}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ac="clr-namespace:Content.Client.Imperial.XxRaay.UI"
<i:BaseImperialWindow
xmlns="https://spacestation14.io"
xmlns:i="clr-namespace:Content.Client.Imperial.UI.Windows.BaseImperialWindow"
HeaderText="{Loc animatronic-controller-window-title}"
🤖 Prompt for AI Agents
In `@Content.Client/Imperial/XxRaay/UI/AnimatronicControllerWindow.xaml` around
lines 4 - 5, Удалите неиспользуемые декларации пространств имён xmlns:controls и
xmlns:ac из XAML в файле AnimatronicControllerWindow.xaml: найдите атрибуты
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" и
xmlns:ac="clr-namespace:Content.Client.Imperial.XxRaay.UI" в корневом элементе и
удалите их, оставив только используемые xmlns, чтобы убрать лишние объявления и
предупреждения.

HeaderText="{Loc animatronic-controller-window-title}"
MinSize="700 400"
SetSize="800 450"
Resizable="True">
<BoxContainer Name="Contents" Access="Public" Orientation="Vertical" Margin="20 20 20 20">
<ScrollContainer VerticalExpand="True" MinHeight="300">
<BoxContainer Name="AnimatronicsList" Access="Public" Orientation="Vertical" SeparationOverride="10" />
</ScrollContainer>
</BoxContainer>
</i:BaseImperialWindow>
295 changes: 295 additions & 0 deletions Content.Client/Imperial/XxRaay/UI/AnimatronicControllerWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
using System.Collections.Generic;
using System.Numerics;
using Content.Client.Imperial.UI.Windows.BaseImperialWindow;
using Content.Shared.Imperial.XxRaay.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Client.GameObjects;

namespace Content.Client.Imperial.XxRaay.UI;

[GenerateTypedNameReferences]
public sealed partial class AnimatronicControllerWindow : BaseImperialWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;

public event Action<NetEntity, NetEntity>? OnAnimatronicWaypointSelected;
public event Action<NetEntity>? OnAnimatronicClearTarget;
public event Action<NetEntity>? OnAnimatronicObserving;

private Dictionary<NetEntity, PanelContainer> _animCards = new();
private Dictionary<NetEntity, Color> _buttonOriginalColors = new();

public AnimatronicControllerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}

public void UpdateState(AnimDataStateEvent state)
{
var animScrollContainer = Contents.GetChild(0) as ScrollContainer;
var animList = animScrollContainer?.GetChild(0) as BoxContainer;

animList?.RemoveAllChildren();
_animCards.Clear();
_buttonOriginalColors.Clear();

foreach (var anim in state.Animatronics)
{
var card = CreateAnimatronicCard(anim, state.Waypoints);
animList?.AddChild(card);
_animCards[anim.Entity] = card;
}
}

private PanelContainer CreateAnimatronicCard(AnimDto anim, List<WaypointDto> waypoints)
{
var card = new PanelContainer
{
StyleClasses = { "AngleRectPass" },
HorizontalExpand = true,
Margin = new Thickness(0, 0, 0, 5)
};

var cardContent = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Margin = new Thickness(10, 10, 10, 10),
SeparationOverride = 0,
HorizontalExpand = true
};

var spritePanel = new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#2A2A2A")
},
VerticalExpand = true
};

var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
Scale = new Vector2(5f, 5f),
SetSize = new Vector2(96, 96),
Margin = new Thickness(5, 5, 5, 5),
VerticalExpand = true,
VerticalAlignment = VAlignment.Center
};

var entity = _entityManager.GetEntity(anim.Entity);
if (entity.Valid)
{
spriteView.SetEntity(entity);
}
Comment on lines +81 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Нет обработки случая невалидной entity.

Если entity.Valid == false, spriteView останется пустым без визуальной индикации. Стоит рассмотреть отображение placeholder-текста или иконки ошибки для отладки.

🤖 Prompt for AI Agents
In `@Content.Client/Imperial/XxRaay/UI/AnimatronicControllerWindow.xaml.cs` around
lines 87 - 91, The code currently only handles the valid case of
_entityManager.GetEntity(anim.Entity); add an else branch for when entity.Valid
== false to avoid leaving spriteView empty: clear any previous entity via
spriteView.SetEntity(null) or spriteView.ClearEntity, then set a visible
placeholder or error indicator (e.g., spriteView.ShowPlaceholder("Invalid
entity") or spriteView.SetErrorIcon) so the UI shows that the entity is invalid;
update the block around _entityManager.GetEntity, entity.Valid, and
spriteView.SetEntity accordingly.


spritePanel.AddChild(spriteView);

var nameStatusPanel = new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#333333")
},
HorizontalExpand = true,
VerticalExpand = true
};

var nameStatusContent = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
Margin = new Thickness(5, 5, 5, 5),
SeparationOverride = 10,
HorizontalExpand = true
};

var nameStatusContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
VerticalExpand = true,
VerticalAlignment = VAlignment.Center
};

var nameLabel = new Label
{
Text = anim.DisplayName,
StyleClasses = { "LabelSubTextPassExtraBoldServerName" },
ModulateSelfOverride = Color.White
};

var statusContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
SeparationOverride = 3
};

if (anim.CurrentWaypointId != null)
{
var dotsLabel = new Label
{
Text = Loc.GetString("animatronic-controller-waypoint-separator"),
StyleClasses = { "LabelSubTextPassMediumBuy" },
ModulateSelfOverride = Color.FromHex("#FFD700"),
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(0, 0, 3, 0)
};

var waypointLabel = new Label
{
Text = anim.CurrentWaypointId,
StyleClasses = { "LabelSubTextPassMediumBuy" },
ModulateSelfOverride = Color.FromHex("#FFD700"),
VerticalAlignment = VAlignment.Center
};

statusContainer.AddChild(dotsLabel);
statusContainer.AddChild(waypointLabel);
}
else
{
var statusLabel = new Label
{
Text = Loc.GetString("animatronic-controller-status-idle"),
StyleClasses = { "LabelSubTextPassMediumBuy" },
ModulateSelfOverride = Color.FromHex("#888888")
};
statusContainer.AddChild(statusLabel);
}

nameStatusContainer.AddChild(nameLabel);
nameStatusContainer.AddChild(statusContainer);

var waypointButtonsContent = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
SeparationOverride = 5,
HorizontalExpand = true
};

var waypointGrid = new GridContainer
{
Columns = 2,
HorizontalExpand = true
};

foreach (var wp in waypoints)
{
var isActive = anim.CurrentWaypointId == wp.WaypointId;
var wpButton = new Button
{
Text = wp.WaypointId,
HorizontalExpand = true
};

var originalColor = isActive ? Color.FromHex("#4CAF50") : Color.FromHex("#888888");
_buttonOriginalColors[wp.Entity] = originalColor;
wpButton.ModulateSelfOverride = originalColor;

wpButton.StyleClasses.Add("ButtonOpenBoth");

var animEntity = anim.Entity;
var wpEntity = wp.Entity;

wpButton.OnPressed += _ =>
{
OnAnimatronicWaypointSelected?.Invoke(animEntity, wpEntity);
};

wpButton.OnMouseEntered += _ =>
{
if (!isActive)
wpButton.ModulateSelfOverride = Color.FromHex("#AAAAAA");
};

wpButton.OnMouseExited += _ =>
{
if (!isActive && _buttonOriginalColors.TryGetValue(wpEntity, out var original))
wpButton.ModulateSelfOverride = original;
};

waypointGrid.AddChild(wpButton);
}

var buttonsContainer = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
SeparationOverride = 5,
VerticalExpand = true
};

var clearButton = new Button
{
Text = Loc.GetString("animatronic-controller-button-clear"),
StyleClasses = { "ButtonOpenBoth" },
ModulateSelfOverride = Color.FromHex("#FF4444"),
VerticalExpand = true,
HorizontalExpand = true
};

var clearButtonOriginalColor = Color.FromHex("#FF4444");
var animEntityForClear = anim.Entity;
clearButton.OnPressed += _ =>
{
OnAnimatronicClearTarget?.Invoke(animEntityForClear);
};

clearButton.OnMouseEntered += _ =>
{
clearButton.ModulateSelfOverride = Color.FromHex("#FF6666");
};

clearButton.OnMouseExited += _ =>
{
clearButton.ModulateSelfOverride = clearButtonOriginalColor;
};

var observingButton = new Button
{
Text = Loc.GetString("animatronic-controller-button-observing"),
StyleClasses = { "ButtonOpenBoth" },
ModulateSelfOverride = Color.FromHex("#4A90E2"),
VerticalExpand = true,
HorizontalExpand = true
};

var observingButtonOriginalColor = Color.FromHex("#4A90E2");
var animEntityForObserving = anim.Entity;
observingButton.OnPressed += _ =>
{
OnAnimatronicObserving?.Invoke(animEntityForObserving);
};

observingButton.OnMouseEntered += _ =>
{
observingButton.ModulateSelfOverride = Color.FromHex("#6BA3E8");
};

observingButton.OnMouseExited += _ =>
{
observingButton.ModulateSelfOverride = observingButtonOriginalColor;
};

buttonsContainer.AddChild(clearButton);
buttonsContainer.AddChild(observingButton);

waypointButtonsContent.AddChild(waypointGrid);
waypointButtonsContent.AddChild(buttonsContainer);

nameStatusContent.AddChild(nameStatusContainer);
nameStatusContent.AddChild(waypointButtonsContent);
nameStatusPanel.AddChild(nameStatusContent);

cardContent.AddChild(spritePanel);
cardContent.AddChild(nameStatusPanel);
card.AddChild(cardContent);

return card;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Дублирование hex-констант цветов.

Одни и те же цветовые коды ("#888888", "#4CAF50", "#FF4444", "#4A90E2" и др.) разбросаны по методу. Рекомендуется вынести их в static readonly Color поля класса для единообразия и простоты изменения.

♻️ Пример
+	private static readonly Color ColorInactive = Color.FromHex("#888888");
+	private static readonly Color ColorActive = Color.FromHex("#4CAF50");
+	private static readonly Color ColorClear = Color.FromHex("#FF4444");
+	private static readonly Color ColorClearHover = Color.FromHex("#FF6666");
+	private static readonly Color ColorObserving = Color.FromHex("#4A90E2");
+	private static readonly Color ColorObservingHover = Color.FromHex("#6BA3E8");
+	private static readonly Color ColorHover = Color.FromHex("#AAAAAA");
+	private static readonly Color ColorGold = Color.FromHex("#FFD700");
🤖 Prompt for AI Agents
In `@Content.Client/Imperial/XxRaay/UI/AnimatronicControllerWindow.xaml.cs` around
lines 51 - 294, CreateAnimatronicCard contains duplicated hex color literals
(e.g. "#888888", "#4CAF50", "#FF4444", "#4A90E2") scattered through the method;
refactor by adding static readonly Color fields on the class (e.g.
AnimatronicControllerWindow.IdleColor, ActiveWaypointColor, ClearButtonColor,
ObservingButtonColor, HoverColor) and replace all literal Color.FromHex(...)
usages (including where you assign originalColor, clearButtonOriginalColor,
observingButtonOriginalColor, and entries in _buttonOriginalColors) with those
constants so colors are centralized and easily changed.

}
Loading
Loading