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: Добавил оффер компонент для того, чтобы сущности могли оффер делать предметов
+```
+
+
+# Ну вот и все
+
+Надеюсь вам понятна документация и вы ею воспользуетесь
+