diff --git "a/_ECHO/Code docs/\320\224\320\276\320\272\321\203\320\274\320\265\320\275\321\202\320\260\321\206\320\270\321\217 \320\272 \321\201\320\270\321\201\321\202\320\265\320\274\320\265 \320\277\320\265\321\200\320\265\320\264\320\260\321\207\320\270 \320\277\321\200\320\265\320\264\320\274\320\265\321\202\320\276\320\262.md" "b/_ECHO/Code docs/\320\224\320\276\320\272\321\203\320\274\320\265\320\275\321\202\320\260\321\206\320\270\321\217 \320\272 \321\201\320\270\321\201\321\202\320\265\320\274\320\265 \320\277\320\265\321\200\320\265\320\264\320\260\321\207\320\270 \320\277\321\200\320\265\320\264\320\274\320\265\321\202\320\276\320\262.md" new file mode 100644 index 0000000..b73636b --- /dev/null +++ "b/_ECHO/Code docs/\320\224\320\276\320\272\321\203\320\274\320\265\320\275\321\202\320\260\321\206\320\270\321\217 \320\272 \321\201\320\270\321\201\321\202\320\265\320\274\320\265 \320\277\320\265\321\200\320\265\320\264\320\260\321\207\320\270 \320\277\321\200\320\265\320\264\320\274\320\265\321\202\320\276\320\262.md" @@ -0,0 +1,451 @@ +## Система передачи из рук в руки (Offer Item System) - документация +Данный документ — сводка информации по новым файлам, системам/компонентам и изменениям в существующих файлах связанным с системой передачи предметов. + +# Оглавление +1. [Суть системы](#сутьсистемы) +2. [Архитектура](#архитектура) +3. [Server](#сервер) +4. [Shared](#шеред) +5. [Client](#клиент) +6. [Что было изменено в офф. файлах](#оффы) +7. [Изменения Yaml и в текстурах](#ямл) + + +### Суть системы +После нажатия на бинд OfferItem на F, у передающего появляется оверлей OfferItemIndicatorsOverlay, переключая в OfferItemComponent IsInOfferMode. После клика по тому, кому надо передать предмет, у клиента появляется алерт. При клике на алерт предмет передается IsInOfferMode становится на офф + + +## Архитектура +``` +Схема потока данных: + +[Игрок нажимает на F] + │ + ▼ +[Shared: SharedOfferItemSystem.Interactions.SetInOfferMode()] + └── Если до этого были какие либо данные в компоненте, обнуляем, если еще не в оффер моде - переводим + + │ + ▼ +[Shared: SharedOfferItemSystem.SetInRecieveMode()] + └── За счет InteractUsingEvent, который вызывается при клике в оффер моде по сущности, заполняем Target в компоненте вставляя в него сущность цель, выводим из оффер мода если тот был у цели + + │ + ▼ +[Server: OfferItemSystem.Update()] + ├── каждый кадр проверяем таймер + ├── Если есть рука и нету предмета = обнуляем все и отменяем оффер + └── Выдаем алерт или убираем его если в режиме получения или нет + │ + ▼ +[Shared: SharedOfferItemSystem.OnClickAlertEvent()] + └── При клике на алерт переходим в метод Recieve + │ + ▼ +[Shared: SharedOfferItemSystem.Recieve()] + └── После проверок передаем предмет и переходим в UnRecieve + │ + ▼ +[Shared: SharedOfferItemSystem.Recieve()] + └── После проверок обнуляем все и отменяем передачу +``` + + +## Server +Своеобразный менеджер, каждый кадр, после таймера проверяем сущности на то, есть ли у них руки и так далее, а после если нету, обнуляем компоненты оффера, или же потом или показываем, если offerItem.IsInReceiveMode=true, или же очищаем алерт оффера + + +## Shared +### Interaction +Делаем привязку к бинду воида SetInOfferMode() +``` + private void InitializeInteractions() + { + CommandBinds.Builder + .Bind(ContentKeyFunctions.OfferItem, InputCmdHandler.FromDelegate(SetInOfferMode, handle: false, outsidePrediction: false)) + .Register(); + } +``` +Переключаем оффер мод с проверками +``` + private void SetInOfferMode(ICommonSession? session) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (session is null) + return; + + if (session.AttachedEntity is not { Valid: true } uid || !Exists(uid) || !_actionBlocker.CanInteract(uid, null)) + return; + + if (!TryComp(uid, out var offerItem)) + return; + + if (!TryComp(uid, out var hands) || hands.ActiveHandId is null) + return; + + offerItem.Item = _hand.GetActiveItem((uid, hands)); + + if (!offerItem.IsInOfferMode) + { + if (offerItem.Item is null) + { + _popup.PopupClient(Loc.GetString("offer-item-empty-hand"), uid, uid); + return; + } + + if (offerItem.Hand is null || offerItem.Target is null) + { + offerItem.IsInOfferMode = true; + offerItem.Hand = hands.ActiveHandId; + + Dirty(uid, offerItem); + return; + } + } + + if (offerItem.Target is not null) + { + UnReceive(offerItem.Target.Value, offerItem: offerItem); + offerItem.IsInOfferMode = false; + Dirty(uid, offerItem); + return; + } + + UnOffer(uid, offerItem); + } +``` +## System +Привязка к интеракту, перемещению игрока, клику по ивенту +``` + public override void Initialize() + { + SubscribeLocalEvent(SetInReceiveMode); + SubscribeLocalEvent(OnMove); + + InitializeInteractions(); + + SubscribeLocalEvent(OnClickAlertEvent); + } +``` +При клике на алерт обращаемся к войду и получаем предмет +``` + private void OnClickAlertEvent(Entity ent, ref AcceptOfferAlertEvent ev) + { + if (ev.Handled || ev.AlertId != OfferAlert) + return; + ev.Handled = true; + Receive(ent!); + } +``` +Проверки и перемещение в руки с попапом предмета +``` + public void Receive(Entity ent) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (!Resolve(ent, ref ent.Comp)) + return; + + if (!TryComp(ent.Comp.Target, out var offerItem)) + return; + + if (offerItem.Hand is null) + return; + + if (ent.Comp.Target is null) + return; + + if (!TryComp(ent, out var hands)) + return; + + if (offerItem.Item is not null) + { + if (!_hand.TryPickup(ent, offerItem.Item.Value, handsComp: hands)) + { + _popup.PopupClient(Loc.GetString("offer-item-full-hand"), ent, ent); + return; + } + + _popup.PopupClient(Loc.GetString("offer-item-give", + ("item", Identity.Entity(offerItem.Item.Value, EntityManager)), + ("target", Identity.Entity(ent, EntityManager))), ent.Comp.Target.Value, ent.Comp.Target.Value); + + _popup.PopupPredicted(Loc.GetString("offer-item-give-other", + ("user", Identity.Entity(ent.Comp.Target.Value, EntityManager)), + ("item", Identity.Entity(offerItem.Item.Value, EntityManager)), + ("target", Identity.Entity(ent, EntityManager))) + , ent.Comp.Target.Value, ent); + } + + offerItem.Item = null; + Dirty(ent); + UnReceive(ent, ent, offerItem); + } +``` +Если выходим за пределы максимальной дистанции, отключаем офер +``` + private void OnMove(EntityUid uid, OfferItemComponent component, MoveEvent args) + { + if (component.Target is null || + _transform.InRange(args.NewPosition, + Transform(component.Target.Value).Coordinates, + component.MaxOfferDistance) + ) + return; + + UnOffer(uid, component); + } +``` + +Делаем офферы и ресетаем все значения с попапами +``` + protected void UnOffer(EntityUid uid, OfferItemComponent component) + { + if (!TryComp(uid, out var hands) || hands.ActiveHandId is null) + return; + + if (TryComp(component.Target, out var offerItem) && component.Target is not null) + { + if (component.Item is not null) + { + if (!_timing.IsFirstTimePredicted) + { + _popup.PopupClient(Loc.GetString("offer-item-no-give", + ("item", Identity.Entity(component.Item.Value, EntityManager)), + ("target", Identity.Entity(component.Target.Value, EntityManager))), uid, uid); + _popup.PopupClient(Loc.GetString("offer-item-no-give-target", + ("user", Identity.Entity(uid, EntityManager)), + ("item", Identity.Entity(component.Item.Value, EntityManager))), uid, component.Target.Value); + } + + } + else if (offerItem.Item is not null) + if (!_timing.IsFirstTimePredicted) + { + _popup.PopupClient(Loc.GetString("offer-item-no-give", + ("item", Identity.Entity(offerItem.Item.Value, EntityManager)), + ("target", Identity.Entity(uid, EntityManager))), component.Target.Value, component.Target.Value); + _popup.PopupClient(Loc.GetString("offer-item-no-give-target", + ("user", Identity.Entity(component.Target.Value, EntityManager)), + ("item", Identity.Entity(offerItem.Item.Value, EntityManager))), component.Target.Value, uid); + } + + offerItem.IsInOfferMode = false; + offerItem.IsInReceiveMode = false; + offerItem.Hand = null; + offerItem.Target = null; + offerItem.Item = null; + + Dirty(component.Target.Value, offerItem); + } + + component.IsInOfferMode = false; + component.IsInReceiveMode = false; + component.Hand = null; + component.Target = null; + component.Item = null; + + Dirty(uid, component); + } +``` + +Проверки и ануление всех переменных +``` + protected void UnReceive(EntityUid uid, OfferItemComponent? component = null, OfferItemComponent? offerItem = null) + { + if (component is null && !TryComp(uid, out component)) + return; + + if (offerItem is null && !TryComp(component.Target, out offerItem)) + return; + + if (!TryComp(uid, out var hands) || hands.ActiveHandId is null || + component.Target is null) + return; + + if (offerItem.Item is not null) + { + _popup.PopupClient(Loc.GetString("offer-item-no-give", + ("item", Identity.Entity(offerItem.Item.Value, EntityManager)), + ("target", Identity.Entity(uid, EntityManager))), component.Target.Value, component.Target.Value); + _popup.PopupClient(Loc.GetString("offer-item-no-give-target", + ("user", Identity.Entity(component.Target.Value, EntityManager)), + ("item", Identity.Entity(offerItem.Item.Value, EntityManager))), component.Target.Value, uid); + } + + if (!offerItem.IsInReceiveMode) + { + offerItem.Target = null; + component.Target = null; + } + + offerItem.Item = null; + offerItem.Hand = null; + component.IsInReceiveMode = false; + + Dirty(uid, component); + } +``` + +Проверка на то, в режиме предложения ли +``` + protected bool IsInOfferMode(EntityUid? entity, OfferItemComponent? component = null) + { + return entity is not null && Resolve(entity.Value, ref component, false) && component.IsInOfferMode; + } +``` + +## Component +``` + [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] + public bool IsInOfferMode; + + [DataField, AutoNetworkedField] + public bool IsInReceiveMode; + + [DataField, AutoNetworkedField] + public string? Hand; + + [DataField, AutoNetworkedField] + public EntityUid? Item; + + [DataField, AutoNetworkedField] + public EntityUid? Target; + + [DataField] + public float MaxOfferDistance = 2f; +``` + +## Events +Ивент принятия офера при нажатии на алерт +``` +public sealed partial class AcceptOfferAlertEvent : BaseAlertEvent; +``` + + +### Client +## IndicatorsOverlay +переменные: +``` + private readonly IInputManager _inputManager; + private readonly IEntityManager _entMan; + private readonly IEyeManager _eye; + private readonly OfferItemSystem _offer; + + private readonly Texture _sight; + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + + private readonly Color _mainColor = Color.White.WithAlpha(0.3f); + private readonly Color _strokeColor = Color.Black.WithAlpha(0.5f); + private readonly float _scale = 0.6f; // 1 is a little big +``` + +сам оверлей + +``` + public OfferItemIndicatorsOverlay(IInputManager input, IEntityManager entMan, IEyeManager eye, OfferItemSystem offerSys) + { + _inputManager = input; + _entMan = entMan; + _eye = eye; + _offer = offerSys; + + var spriteSys = _entMan.EntitySysManager.GetEntitySystem(); + _sight = spriteSys.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/_ECHO/Misc/give_item.rsi"), "give_item")); + } +``` + +Ну и операции по перед отрисовкой, отрисовка сама, и сайгхт +``` + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + return _offer.IsInOfferMode() && base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var mousePosMap = _eye.PixelToMap(_inputManager.MouseScreenPosition); + + if (mousePosMap.MapId != args.MapId) + return; + + var limitedScale = Math.Min(1.25f, (args.ViewportControl as Control)?.UIScale ?? 1f) * _scale; + + DrawSight(_sight, args.ScreenHandle, _inputManager.MouseScreenPosition.Position, limitedScale); + } + + private void DrawSight(Texture sight, DrawingHandleScreen screen, Vector2 centerPos, float scale) + { + var sightSize = sight.Size * scale; + var expandedSize = sightSize + new Vector2(7); + + screen.DrawTextureRect(sight, UIBox2.FromDimensions(centerPos - sightSize * 0.5f, sightSize), _strokeColor); + screen.DrawTextureRect(sight, UIBox2.FromDimensions(centerPos - expandedSize * 0.5f, expandedSize), _mainColor); + } +``` + + +## Что было изменено в офф. файлах +### Content.Shared/Input/ContentKeyFunctions.cs +Бинд +``` +public static readonly BoundKeyFunction OfferItem = "OfferItem"; +``` +### Content.Client/Input/ContentContext.cs +``` +human.AddFunction(ContentKeyFunctions.OfferItem); +``` + +### Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +``` +AddButton(ContentKeyFunctions.OfferItem); +``` + + +## Изменения Yaml +### Алерт в /Prototypes/_ECHO/Alerts/alerts.yml +``` +- type: alert + id: Offer + clickEvent: !type:AcceptOfferAlertEvent + icons: + - sprite: /Textures/_ECHO/Alerts/offer_item.rsi + state: offer_item + name: alerts-offer-name + description: alerts-offer-desc +``` +### /Resources/keybinds.yml +Бинды +``` +- function: OfferItem + type: State + key: F +``` +### /Resources/Prototypes/Entities/Mobs/base.yml +Добавил всем контролируемым мобам возможность трейда +``` +- type: entity + abstract: true + save: false + parent: BaseControllable + id: BaseMob + components: + - type: MobCollision + - type: Physics + bodyType: KinematicController + - type: InputMover + - type: MobMover + - type: MovementSpeedModifier + - type: SpeechBarks # ECHO-Tweak : Barks + - type: OfferItem # ECHO-Tweak: Добавил оффер компонент для того, чтобы сущности могли оффер делать предметов +``` + + +# Ну вот и все + +Надеюсь вам понятна документация и вы ею воспользуетесь +