using System.Linq; using System.Numerics; using Content.Client._Sunrise.InteractionsPanel.Models; using Content.Shared._Sunrise.InteractionsPanel.Data.Prototypes; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; using Robust.Client.Graphics; using Robust.Client.Utility; using Robust.Shared.Audio.Systems; using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Client._Sunrise.InteractionsPanel; [GenerateTypedNameReferences] public sealed partial class CustomInteractionEditor : DefaultWindow { #region Color Scheme private static readonly Color PrimaryColor = new Color(0, 212, 255); // #00d4ff private static readonly Color SecondaryColor = new Color(184, 230, 255); // #b8e6ff private static readonly Color BackgroundDark = new Color(15, 20, 25); // #0f1419 private static readonly Color BackgroundLight = new Color(36, 51, 64); // #243340 private static readonly Color BackgroundHighlight = new Color(44, 68, 85); // #2c4455 private static readonly Color TextMuted = new Color(122, 157, 179); // #7a9db3 #endregion #region Dependencies [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] protected readonly ILocalizationManager Loc = default!; [Dependency] private readonly CustomInteractionService _customInteractionService = default!; private readonly SharedAudioSystem _audio = default!; #endregion #region State private CustomInteraction _interaction; private Action? _onClose; private readonly Dictionary _categoryIds = new(); private readonly Dictionary _iconIds = new(); private readonly Dictionary _effectIds = new(); private readonly Dictionary _soundIds = new(); private const int MaxNameLength = 64; private const int MaxDescriptionLength = 256; private const int MaxMessageLength = 128; #endregion #region Constructor public CustomInteractionEditor(CustomInteraction? interaction = null) { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); var iManager = IoCManager.Resolve(); _audio = iManager.EntitySysManager.GetEntitySystem(); var isNewInteraction = interaction == null; _interaction = interaction ?? new CustomInteraction { Id = string.Empty, Name = string.Empty, Description = string.Empty, IconId = string.Empty, CategoryId = string.Empty, InteractionMessages = new List(), SoundIds = new List(), SpawnsEffect = false, EffectChance = 0.5f, EntityEffectId = string.Empty, Cooldown = 5.0f }; Title = isNewInteraction ? "Создание взаимодействия" : "Редактирование взаимодействия"; InitializeUI(); InitializeEvents(); LoadData(); } #endregion #region Initialization private void InitializeUI() { EffectChanceSlider.OnValueChanged += OnEffectChanceChanged; CooldownSlider.OnValueChanged += OnCooldownChanged; AddMessageButton.OnPressed += OnAddMessagePressed; AddSoundButton.OnPressed += OnAddSoundPressed; SaveButton.OnPressed += OnSavePressed; CancelButton.OnPressed += OnCancelPressed; TestSoundButton.OnPressed += OnTestSoundPressed; SpawnsEffectCheckBox.OnToggled += OnSpawnsEffectToggled; CategoryOption.OnItemSelected += OnCategorySelected; IconOption.OnItemSelected += OnIconSelected; EffectOption.OnItemSelected += OnEffectSelected; SoundOption.OnItemSelected += OnSoundSelected; UpdateEffectChanceLabel(EffectChanceSlider.Value); UpdateCooldownLabel(CooldownSlider.Value); } private void OnCategorySelected(OptionButton.ItemSelectedEventArgs args) { CategoryOption.SelectId(args.Id); } private void OnIconSelected(OptionButton.ItemSelectedEventArgs args) { IconOption.SelectId(args.Id); UpdateIconPreview(); } private void OnEffectSelected(OptionButton.ItemSelectedEventArgs args) { EffectOption.SelectId(args.Id); } private void OnSoundSelected(OptionButton.ItemSelectedEventArgs args) { SoundOption.SelectId(args.Id); } private void InitializeEvents() { SpawnsEffectCheckBox.OnToggled += _ => UpdateEffectControlsEnabled(); } private void LoadData() { _categoryIds.Clear(); _iconIds.Clear(); _effectIds.Clear(); _soundIds.Clear(); CategoryOption.Clear(); int categoryId = 0; CategoryOption.AddItem("Выберите категорию"); _categoryIds[categoryId] = string.Empty; categoryId++; foreach (var category in _prototypeManager.EnumeratePrototypes().OrderBy(c => Loc.GetString(c.Name))) { _categoryIds[categoryId] = category.ID; CategoryOption.AddItem(Loc.GetString(category.Name), categoryId); if (category.ID == _interaction.CategoryId) { CategoryOption.SelectId(categoryId); } categoryId++; } IconOption.Clear(); int iconId = 0; IconOption.AddItem("Нет"); _iconIds[iconId] = string.Empty; iconId++; foreach (var icon in _prototypeManager.EnumeratePrototypes().OrderBy(i => Loc.GetString(i.Name))) { _iconIds[iconId] = icon.ID; IconOption.AddItem(Loc.GetString(icon.Name), iconId); if (icon.ID == _interaction.IconId) { IconOption.SelectId(iconId); } iconId++; } EffectOption.Clear(); int effectId = 0; EffectOption.AddItem("Нет"); _effectIds[effectId] = string.Empty; effectId++; foreach (var effect in _prototypeManager.EnumeratePrototypes().OrderBy(e => Loc.GetString(e.Name))) { _effectIds[effectId] = effect.ID; EffectOption.AddItem(Loc.GetString(effect.Name), effectId); if (effect.ID == _interaction.EntityEffectId) { EffectOption.SelectId(effectId); } effectId++; } SoundOption.Clear(); int soundId = 0; SoundOption.AddItem("Выберите звук"); _soundIds[soundId] = string.Empty; soundId++; foreach (var sound in _prototypeManager.EnumeratePrototypes().OrderBy(s => Loc.GetString(s.Name))) { _soundIds[soundId] = sound.ID; SoundOption.AddItem(Loc.GetString(sound.Name), soundId); soundId++; } NameInput.Text = _interaction.Name; DescriptionInput.Text = _interaction.Description; SpawnsEffectCheckBox.Pressed = _interaction.SpawnsEffect; EffectChanceSlider.Value = _interaction.EffectChance; CooldownSlider.Value = _interaction.Cooldown; MessagesContainer.DisposeAllChildren(); foreach (var message in _interaction.InteractionMessages) { AddMessageToList(message); } SoundsContainer.DisposeAllChildren(); foreach (var soundId2 in _interaction.SoundIds) { if (_prototypeManager.TryIndex(soundId2, out var soundProto)) { AddSoundToList(soundId2, soundProto.Name); } } UpdateEffectControlsEnabled(); UpdateIconPreview(); } #endregion #region UI Updates private void UpdateEffectChanceLabel(float value) { var percentage = (int)(value * 100); EffectChanceLabel.Text = $"{percentage}%"; } private void UpdateCooldownLabel(float value) { CooldownLabel.Text = $"{(int)value}с"; } private void UpdateEffectControlsEnabled() { var enabled = SpawnsEffectCheckBox.Pressed; EffectChanceSlider.Disabled = !enabled; EffectOption.Disabled = !enabled; EffectChanceLabel.Modulate = enabled ? Color.White : TextMuted; } private void UpdateIconPreview() { if (!_iconIds.TryGetValue(IconOption.SelectedId, out var iconId) || string.IsNullOrEmpty(iconId)) { IconPreview.Texture = null; return; } if (_prototypeManager.TryIndex(iconId, out var iconProto)) { IconPreview.Texture = iconProto.Icon.Frame0(); } } #endregion #region Event Handlers private void OnEffectChanceChanged(Robust.Client.UserInterface.Controls.Range range) { UpdateEffectChanceLabel(range.Value); } private void OnCooldownChanged(Robust.Client.UserInterface.Controls.Range range) { UpdateCooldownLabel(range.Value); } private void OnSpawnsEffectToggled(BaseButton.ButtonToggledEventArgs args) { UpdateEffectControlsEnabled(); } private void OnAddMessagePressed(BaseButton.ButtonEventArgs args) { var message = NewMessageInput.Text.Trim(); if (string.IsNullOrEmpty(message)) return; if (message.Length > MaxMessageLength) { ShowError($"Сообщение слишком длинное (макс. {MaxMessageLength} символов)"); return; } _interaction.InteractionMessages.Add(message); AddMessageToList(message); NewMessageInput.Text = string.Empty; } private void OnAddSoundPressed(BaseButton.ButtonEventArgs args) { if (SoundOption.SelectedId == 0) return; if (!_soundIds.TryGetValue(SoundOption.SelectedId, out var selectedSoundId) || string.IsNullOrEmpty(selectedSoundId)) return; if (_interaction.SoundIds.Contains(selectedSoundId)) return; if (_prototypeManager.TryIndex(selectedSoundId, out var soundProto)) { _interaction.SoundIds.Add(selectedSoundId); AddSoundToList(selectedSoundId, soundProto.Name); } } private void OnTestSoundPressed(BaseButton.ButtonEventArgs args) { PlayTestSound(); } private void OnSavePressed(BaseButton.ButtonEventArgs args) { if (string.IsNullOrEmpty(NameInput.Text.Trim())) { ShowError("Имя взаимодействия не может быть пустым"); return; } if (_interaction.Name.Length > MaxNameLength) { ShowError($"Имя слишком длинное (макс. {MaxNameLength} символов)"); return; } if (_interaction.Description.Length > MaxDescriptionLength) { ShowError($"Описание слишком длинное (макс. {MaxDescriptionLength} символов)"); return; } if (_interaction.InteractionMessages.Count == 0) { ShowError("Добавьте хотя бы одно сообщение взаимодействия"); return; } if (!_categoryIds.TryGetValue(CategoryOption.SelectedId, out var categoryId) || string.IsNullOrEmpty(categoryId)) { ShowError("Выберите категорию"); return; } _interaction.Name = NameInput.Text.Trim(); _interaction.Description = DescriptionInput.Text.Trim(); _interaction.CategoryId = categoryId; _iconIds.TryGetValue(IconOption.SelectedId, out var iconId); _interaction.IconId = iconId ?? string.Empty; _interaction.SpawnsEffect = SpawnsEffectCheckBox.Pressed; _interaction.EffectChance = EffectChanceSlider.Value; _effectIds.TryGetValue(EffectOption.SelectedId, out var effectId); _interaction.EntityEffectId = effectId ?? string.Empty; _interaction.Cooldown = CooldownSlider.Value; _customInteractionService.SaveInteraction(_interaction); _onClose?.Invoke(true); Close(); } private void OnCancelPressed(BaseButton.ButtonEventArgs args) { _onClose?.Invoke(false); Close(); } #endregion #region Helper Methods private void AddMessageToList(string message) { var panelContainer = new PanelContainer { Margin = new Thickness(0, 1, 0, 1), HorizontalExpand = true, MinHeight = 24 }; panelContainer.PanelOverride = new StyleBoxFlat { BackgroundColor = BackgroundHighlight, BorderColor = BackgroundLight, BorderThickness = new Thickness(1) }; var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal, HorizontalExpand = true, VerticalAlignment = VAlignment.Center, Margin = new Thickness(6, 2) }; var label = new Label { Text = message, HorizontalExpand = true, FontColorOverride = Color.White, ClipText = true, VerticalAlignment = VAlignment.Center, Margin = new Thickness(0, 0, 4, 0) }; var removeButton = new Button { Text = "✕", StyleClasses = { StyleClass.ButtonSquare }, MinWidth = 22, MinHeight = 22, Label = { FontColorOverride = PrimaryColor} }; removeButton.OnPressed += _ => { _interaction.InteractionMessages.Remove(message); panelContainer.Dispose(); }; container.AddChild(label); container.AddChild(removeButton); panelContainer.AddChild(container); MessagesContainer.AddChild(panelContainer); } private void AddSoundToList(string soundId, string soundName) { var panelContainer = new PanelContainer { Margin = new Thickness(0, 1, 0, 1), HorizontalExpand = true, MinHeight = 24 }; panelContainer.PanelOverride = new StyleBoxFlat { BackgroundColor = BackgroundHighlight, BorderColor = BackgroundLight, BorderThickness = new Thickness(1) }; var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal, HorizontalExpand = true, VerticalAlignment = VAlignment.Center, Margin = new Thickness(6, 2) }; var label = new Label { Text = soundName, HorizontalExpand = true, FontColorOverride = Color.White, ClipText = true, VerticalAlignment = VAlignment.Center, Margin = new Thickness(0, 0, 4, 0) }; var removeButton = new Button { Text = "✕", StyleClasses = { StyleClass.ButtonSquare }, MinWidth = 22, MinHeight = 22, Label = { FontColorOverride = PrimaryColor} }; removeButton.OnPressed += _ => { _interaction.SoundIds.Remove(soundId); panelContainer.Dispose(); }; container.AddChild(label); container.AddChild(removeButton); panelContainer.AddChild(container); SoundsContainer.AddChild(panelContainer); } private void ShowError(string message) { var dialog = new DefaultWindow { Title = "Ошибка", MinSize = new Vector2(300, 150), SetSize = new Vector2(300, 150) }; var dialogPanel = new PanelContainer { VerticalExpand = true, HorizontalExpand = true }; dialogPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = BackgroundDark }; var dialogVBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Vertical, Margin = new Thickness(16), VerticalExpand = true }; var errorLabel = new Label { Text = message, HorizontalExpand = true, HorizontalAlignment = HAlignment.Center, VerticalAlignment = VAlignment.Center, Margin = new Thickness(0, 0, 0, 16), FontColorOverride = SecondaryColor }; var closeButton = new Button { Text = "ОК", StyleClasses = { StyleClass.ButtonSquare }, HorizontalAlignment = HAlignment.Center, MinWidth = 80, MinHeight = 30 }; closeButton.OnPressed += _ => dialog.Close(); dialogVBox.AddChild(errorLabel); dialogVBox.AddChild(closeButton); dialogPanel.AddChild(dialogVBox); dialog.AddChild(dialogPanel); dialog.OpenCentered(); } private void PlayTestSound() { if (!_soundIds.TryGetValue(SoundOption.SelectedId, out var selectedSoundId) || string.IsNullOrEmpty(selectedSoundId)) return; if (_prototypeManager.TryIndex(selectedSoundId, out var soundProto)) { _audio.PlayGlobal(soundProto.Sound, Filter.Local(), false); } } #endregion #region Public Methods public void SetCloseCallback(Action callback) { _onClose = callback; } #endregion }