// © SUNRISE, An EULA/CLA with a hosting restriction, full text: https://github.com/space-sunrise/space-station-14/blob/master/CLA.txt using System.Linq; using Content.Shared.Actions; using Content.Shared.Bed.Sleep; using Content.Shared.Cloning.Events; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared._Sunrise.Pets; public abstract class SharedPettingSystem : EntitySystem { [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly IGameTiming _timing = default!; // Стандартный приказ, выдающийся при приручении private const PetOrderType DefaultOrder = PetOrderType.Follow; private const PetOrderType AttackOrder = PetOrderType.Attack; // Айди акшенов, которые будут выдаваться хозяину при появлении питомца. private static readonly EntProtoId OpenUiAction = "PetOpenAllUiAction"; private static readonly EntProtoId AttackTargetAction = "PetAttackTargetAction"; // Эффекты private static readonly EntProtoId PettingSuccessEffect = "EffectHearts"; public override void Initialize() { base.Initialize(); // Приручение SubscribeLocalEvent(OnPetAttempt); // Управление SubscribeLocalEvent(OnPetUiActionToggled); SubscribeLocalEvent(OnPetAttackToggled); // Совместимость SubscribeLocalEvent(OnMasterCloned); SubscribeLocalEvent(OnMasterShutdown); SubscribeLocalEvent(OnPetShutdown); SubscribeLocalEvent(OnLoadoutSpawn); } #region Base petting private void OnPetAttempt(Entity master, ref BeforeInteractHandEvent args) { // Эта проверка позволяет методу срабатывать на клиенте только в первый раз, когда он предугадывает его. // Нужно, чтобы клиент 999 раз не выполнял действия ниже if (!_timing.IsFirstTimePredicted) return; // Проверяем возможность приручения у таргета. if (!TryComp(args.Target, out var pet)) return; // Проверяем, спит ли таргет if (HasComp(args.Target)) return; // Проверяем, жив ли таргет. if (!_mobState.IsAlive(args.Target)) return; // Объявляем перменные, чтобы несколько раз не передавать ее длинную расшифровку. var petEntity = (args.Target, pet); // Пытаемся задать питомцу хозязина и проверяем, получилось ли. if (!TrySetMaster(petEntity, master)) return; // Приручаем питомца, если все прошло успешно и код дошел до этого момента. Pet(petEntity); } /// /// Задает владельца питомца и вызывает ивент, сообщающий об этом /// /// Питомец /// Владелец /// Следует ли перезаписать текущего владельца, если он есть? public bool TrySetMaster(Entity pet, Entity master, bool overrideMaster = false) { // По умолчанию сменить владельца после приручения нельзя if (pet.Comp.Master != null && !overrideMaster) return false; // Меняем владельца pet.Comp.Master = master; Dirty(pet); // Добавляем питомца в список прирученных питомцев master.Comp.Pets.Add(pet); Dirty(master); // Вызываем ивент, который сообщает о смене владельца RaiseLocalEvent(pet, new PetMasterChanged(master)); return true; } /// /// Метод, работающий с логикой отвязывания хозяина от питомца /// /// Питомец /// Хозяин private void RemoveMaster(Entity pet, Entity master) { // Заставляем питомца забыть своего хозяина pet.Comp.Master = null; Dirty(pet); // Заставляем хозяина забыть питомца MasterForgetPet(master, pet); } /// /// Метод, убирающий питомца из списка питомцев хозяина. /// /// Ентити хозяина, у которого мы убираем питомца /// EntityUid питомца, которого мы убираем private void MasterForgetPet(Entity master, EntityUid pet) { // Убираем питомца из списка прирученных питомцев у хозяина master.Comp.Pets.Remove(pet); Dirty(master); // Если после отвязки у хозяина не осталось питомцев... if (master.Comp.Pets.Count != 0) return; // ..Мы убираем у него все акшены, связанные с питомцами. foreach (var actionUid in master.Comp.PetActions) { _actions.RemoveAction(actionUid); } // TODO: Возможность убрать акшен атаки отдельно, когда не осталось питомцев с возможностью атаковать. } /// /// Работает с логикой поведения питомца после приручения /// /// Питомец /// Скрывать визуальные эффекты, звуки и попапы? public void Pet(Entity pet, bool silent = false) { // Проигрываем звук приручения, если он есть if (pet.Comp.PettingSuccessfulSound != null && !silent) _audio.PlayEntity(pet.Comp.PettingSuccessfulSound, pet, pet); // Необходимая проверка, чтобы еще раз убедиться, что хозяин имеется. Даже с учетом того, что он не может не иметься // Так как метод вызывается после присваивания хозяина. var master = pet.Comp.Master; if (!master.HasValue) return; // Показываем игроку попап об успешном приручении if (!silent) { var message = Loc.GetString("pet-success", ("name", Identity.Name(pet, EntityManager, master.Value))); _popup.PopupClient(message, pet.Owner, master.Value); } // Спавним эффект с сердечками if (!silent) Spawn(PettingSuccessEffect, _transform.GetMapCoordinates(pet)); // Если питомцу не доступен стандартный приказ, то он не меняет своей логики поведения if (!pet.Comp.AllowedOrders.Contains(DefaultOrder)) return; // Ивент информирует, что питомцу требуется обновление логики НПС. А она только на сервере RaiseLocalEvent(pet, new PetSetAILogicEvent(DefaultOrder)); // Дальше добавляем акшены для управления питомцем. // Делаем это через код, чтобы они появлялись только после приручения питомца if (!TryComp(master, out var masterComponent)) return; var masterEntity = (master.Value, masterComponent); TryAddOpenUiAction(masterEntity); TryAddAttackAction(masterEntity); } /// /// Добавляет акшен открывающий интерфейсы питомцев. /// Работает только 1 раз, после первого приручения /// /// Entity(PetOnInteractComponent) его хозяина private bool TryAddOpenUiAction(Entity master) { // Если питомцев больше одного - это не первое приручение и акшен уже имеется, разворачиваемся if (master.Comp.Pets.Count != 1) return false; // Добавляем акшен хозяину и добавляем его в список акшенов var action = _actions.AddAction(master, OpenUiAction); if (!action.HasValue) return false; master.Comp.PetActions.Add(action.Value); return true; } /// /// Добавляет акшен выбирающий питомцам цель атаки. /// Работает только 1 раз, после добавления питомца с возможностью атаковать. /// /// Entity(PetOnInteractComponent) его хозяина private bool TryAddAttackAction(Entity master) { // Список всех питомцев, имеющих возможность атаковать var aggressivePetList = master.Comp.Pets.Where(x => TryComp(x, out var petComponent) && petComponent.AllowedOrders.Contains(PetOrderType.Attack)); // Если таких питомцев больше одного - это не первое приручение и акшен уже имеется, разворачиваемся if (aggressivePetList.Count() != 1) return false; // Добавляем акшен хозяину и добавляем его в список акшенов var action = _actions.AddAction(master, AttackTargetAction); if (!action.HasValue) return false; master.Comp.PetActions.Add(action.Value); return true; } private void OnLoadoutSpawn(Entity pet, ref LoadoutPetSpawned args) { if (!_timing.IsFirstTimePredicted) return; if (!TryComp(args.Master, out var masterComponent)) return; var masterEntity = (args.Master, masterComponent); // Пытаемся задать питомцу хозязина и проверяем, получилось ли. if (!TrySetMaster(pet, masterEntity)) return; // Приручаем питомца, если все прошло успешно и код дошел до этого момента. Pet(pet, true); } #endregion #region Petting events /// /// Метод, вызываем при активации акшена хозяина питомцев. /// Акшен должен переключать меню управления всех питомцев хозяина для удобства управления несколькими питомцами. /// /// Entity хозяина /// Ивент, вызываемый акшеном, типа PetOpenAllUiEvent private void OnPetUiActionToggled(Entity master, ref PetOpenAllUiEvent args) { // Выполняем действия для каждого питомца из списка прирученных питомцев у хозяина. foreach (var pet in master.Comp.Pets) { _ui.TryToggleUi(pet, PetControlUiKey.Key, master); } } /// /// Метод, работающий с каждым питомцем, задавая ему приказ атаковать, если это возможно. /// /// Entity хозяина /// Ивент, передающий приказ атаковать и таргет для атаки, типа PetAttackTargetEvent private void OnPetAttackToggled(Entity master, ref PetAttackTargetEvent args) { // Проходимся по всем прирученным питомцам у владельца foreach (var pet in master.Comp.Pets) { // Получаем компонент питомца, чтобы достать оттуда список доступных приказов if (!TryComp(pet, out var petComponent)) continue; // Если питомец не имеет приказа атаковать, то он пропускается if (!petComponent.AllowedOrders.Contains(AttackOrder)) continue; // Вызываем ивент, информирующий о смене логики поведения питомца на приказ атаки, передавая цель. RaiseLocalEvent(pet, new PetSetAILogicEvent(AttackOrder, args.Target)); } } #endregion #region Pets compability /// /// Метод, работающий с логикой передачи всех питомцев от одного хозяина к другому. /// Если нового хозяина нет, метод очищает весь список питомцев у старого владельца и отвязывает их. /// /// Entity старого хозяина /// EntityUid нового хозяина, если есть. В него перейдут все питомцы и добавятся все нужные для этого акшены private void CleanMaster(Entity master, EntityUid? newMaster = null) { // Захешировал список, так как хз, как поведет себя цикл при удалении оттуда питомцев при итерации var oldPets = master.Comp.Pets; // Проходимся по списку всех питомцев у старого владельца foreach (var petUid in oldPets) { if (!TryComp(petUid, out var petComponent)) continue; var petEntity = (petUid, petComponent); // Убираем питомца из списка питомцев старого тела RemoveMaster(petEntity, master); // Если хозяин null, значит мы не передаем питомцев в новое тело. // Потоум что тела или нет, или это нам не требуется if (!newMaster.HasValue) continue; if (!TryComp(newMaster, out var newMasterComponent)) continue; // Новое тела хозяина var newMasterEntity = (newMaster.Value, newMasterComponent); // Добавляем питомца в нового мастера TrySetMaster(petEntity, newMasterEntity, true); // Задаем питомцу правильную логику и добавляем владельцу акшены Pet(petEntity, true); } // TODO: В идеале список питомцев должен храниться в Mind`е хозяина, чтобы автоматически переноситься в новое тело, как только туда зайдет игрок } /// /// Метод, обрабатывающий логику передачи питомцев от старого тела хозяина к новому при клонировании /// /// Entity прошлого тела хозяина /// Ивент типа CloningEvent private void OnMasterCloned(Entity master, ref CloningEvent args) { CleanMaster(master, args.CloneUid); } /// /// Метод, обрабатывающий логику отвязывания питомцев от хозяина, когда его тело гибнули. /// /// Entity хозяина /// Ивент типа ComponentShutdown private void OnMasterShutdown(Entity master, ref ComponentShutdown args) { CleanMaster(master); } /// /// Метод, занимающийся обработкой последствий после компонента питомца или его целиком из реальности. /// /// Entity питомца /// Ивент типа ComponentShutdown private void OnPetShutdown(Entity pet, ref ComponentShutdown args) { var master = pet.Comp.Master; if (!master.HasValue) return; if (!TryComp(master, out var masterComponent)) return; var masterEntity = (master.Value, masterComponent); MasterForgetPet(masterEntity, pet); } #endregion }