592 lines
18 KiB
C#
592 lines
18 KiB
C#
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<bool>? _onClose;
|
||
|
||
private readonly Dictionary<int, string> _categoryIds = new();
|
||
private readonly Dictionary<int, string> _iconIds = new();
|
||
private readonly Dictionary<int, string> _effectIds = new();
|
||
private readonly Dictionary<int, string> _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<IEntityManager>();
|
||
_audio = iManager.EntitySysManager.GetEntitySystem<SharedAudioSystem>();
|
||
|
||
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<string>(),
|
||
SoundIds = new List<string>(),
|
||
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<InteractionCategoryPrototype>().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<InteractionIconPrototype>().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<InteractionEntityEffectPrototype>().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<InteractionSoundPrototype>().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<InteractionSoundPrototype>(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<InteractionIconPrototype>(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<InteractionSoundPrototype>(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<InteractionSoundPrototype>(selectedSoundId, out var soundProto))
|
||
{
|
||
_audio.PlayGlobal(soundProto.Sound, Filter.Local(), false);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Public Methods
|
||
|
||
public void SetCloseCallback(Action<bool> callback)
|
||
{
|
||
_onClose = callback;
|
||
}
|
||
|
||
#endregion
|
||
}
|