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\277\320\276 \320\260\320\275\320\270\320\274\320\260\321\206\320\270\321\217\320\274 \321\201\320\262\320\260\321\200\320\272\320\270.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\277\320\276 \320\260\320\275\320\270\320\274\320\260\321\206\320\270\321\217\320\274 \321\201\320\262\320\260\321\200\320\272\320\270.md" index 13e3a3a..af3967d 100644 --- "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\277\320\276 \320\260\320\275\320\270\320\274\320\260\321\206\320\270\321\217\320\274 \321\201\320\262\320\260\321\200\320\272\320\270.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\277\320\276 \320\260\320\275\320\270\320\274\320\260\321\206\320\270\321\217\320\274 \321\201\320\262\320\260\321\200\320\272\320\270.md" @@ -47,14 +47,14 @@ └── Вызывает UseToolEvent на инструменте (сварке) │ ▼ -[Server: WeldingSparksSystem.OnUseTool()] +[Сервер: WeldingSparksSystem.OnUseTool()] ├── Играет звук сварки ├── Определяет позицию спавна эффекта (координаты цели или клика) ├── Spawn("EchoEffectWeldingSparks") — создаёт сущность-эффект └── RaiseNetworkEvent(SpawnedWeldingSparksEvent) → все клиенты │ ▼ -[Client: WeldingSparksAnimationSystem.OnSpawnedWeldingSparks()] +[Клиент: WeldingSparksAnimationSystem.OnSpawnedWeldingSparks()] ├── Проверяет WeldableComponent + WeldingSparksAnimationComponent на цели ├── Вычисляет начальный и конечный Offset (с учётом поворота и типа действия) └── Запускает Animation (линейная интерполяция SpriteComponent.Offset) @@ -63,7 +63,7 @@ [DoAfter завершился / отменён] │ ▼ -[Server: WeldingSparksSystem.OnAfterUseTool()] +[Сервер: WeldingSparksSystem.OnAfterUseTool()] └── QueueDel(effect) — удаляет сущность-эффект ``` @@ -91,15 +91,16 @@ public readonly record struct UseToolEvent } ``` -**Что это:** Событие, которое поднимается на сварке после того, как DoAfter-таймер успешно запустился. Позволяет модульным системам (вроде `WeldingSparksSystem`) реагировать на начало использования инструмента. +**Что это:** Событие, которое поднимается на инструменте (сварке) после того, как DoAfter-таймер успешно запустился. Позволяет модульным системам (вроде `WeldingSparksSystem`) реагировать на начало использования инструмента. **Ключевые моменты:** - `readonly record struct` — неизменяемая структура-запись. Легковесная: не аллоцирует кучу (heap), значение копируется целиком. `record` автоматически генерирует `Equals`, `GetHashCode`, `ToString`. +- Находится в `Content.Shared` (а не в `Content.Server`), потому что `SharedToolSystem` — общий для сервера и клиента. Событие объявлено рядом с системой, которая его поднимает. **Поля:** - `User` — `EntityUid` того, кто использует инструмент (игрок). -- `Target` — `EntityUid?` цели (объект, на который кликнули). Может быть `null`. -- `DoAfterIdx` — индекс DoAfter-таймера. Через него позже восстанавливается полный `DoAfterId` (пара `(user, index)`), который используется как ключ для отслеживания спавненных эффектов. +- `Target` — `EntityUid?` цели (объект, на который кликнули). Может быть `null` — например при сварке пола, когда нет конкретной сущности-цели. +- `DoAfterIdx` — индекс DoAfter-таймера. Через него позже восстанавливается полный `DoAfterId` (пара `(user, index)`), который используется как ключ словаря для отслеживания спавненных эффектов. - `DoAfterLength` — длительность DoAfter-таймера. Передаётся клиенту, чтобы анимация длилась ровно столько же, сколько сварка. **Почему `ushort DoAfterIdx`, а не полный `DoAfterId`?** @@ -124,9 +125,10 @@ public sealed partial class SpawnedWeldingSparksEvent(NetEntity targetEnt, NetEn **Атрибуты:** - `[Serializable, NetSerializable]` — ОБЯЗАТЕЛЬНО для любого события, отправляемого по сети. Без этого движок не сможет сериализовать объект для передачи. - `EntityEventArgs` — базовый класс для сетевых событий в SS14. +- Используется **primary constructor** синтаксис C# 12: параметры конструктора объявлены прямо в определении класса `(NetEntity targetEnt, NetEntity sparksEnt, TimeSpan duration)`. **Поля:** -- `TargetEnt` — `NetEntity` (сетевой ID) свариваемой сущности (шлюз, стенаx и т.д.). Тип `NetEntity` — это ID сущности в формате, понятном и серверу и клиенту (в отличие от обычного `EntityUid`, который локален для каждой стороны). +- `TargetEnt` — `NetEntity` (сетевой ID) свариваемой сущности (шлюз, стена и т.д.). Тип `NetEntity` — это ID сущности в формате, понятном и серверу и клиенту (в отличие от обычного `EntityUid`, который локален для каждой стороны). - `SparksEnt` — `NetEntity` спавненной сущности-эффекта (искры + дым). Клиент будет анимировать именно её спрайт. - `Duration` — длительность анимации. Совпадает с длительностью DoAfter, чтобы анимация шла ровно столько, сколько длится сварка. @@ -160,7 +162,7 @@ public sealed partial class WeldingSparksComponent : Component **Поля:** - `EffectProto` — ID прототипа сущности-эффекта. По умолчанию `"EchoEffectWeldingSparks"` (обычные оранжевые искры). Для экспериментальной сварки переопределяется на `"EchoEffectWeldingSparksExp"` (бирюзовые искры). Тип `EntProtoId` — строка-обёртка, которая «знает», что это ID прототипа сущности. - `SpawnedEffects` — словарь `Dictionary`, связывающий каждый активный DoAfter с его эффектом. Когда DoAfter завершается или отменяется, система удаляет соответствующий эффект из этого словаря и из мира. -- `LastClickLocation` — последняя позиция клика игрока. Нужна для кейса, что сущность `null`. +- `LastClickLocation` — последняя позиция клика игрока. Нужна для кейса сварки пола (floor tiles): при сварке пола нет конкретной сущности-цели (`target == null`), поэтому эффект спавнится на тайле, куда кликнул игрок. Записывается через событие `BeforeRangedInteractEvent`. **Где добавляется:** В YAML-прототипе сварочного инструмента: ```yaml @@ -188,7 +190,7 @@ public sealed partial class WeldingSparksAnimationComponent : Component } ``` -**Что это:** Компонент, который прикрепляется к **свариваемой** сущности (шлюз, стены и т.д.). Определяет, как именно искры будут двигаться по объекту во время сварки. +**Что это:** Компонент, который прикрепляется к **свариваемой** сущности (шлюз, шаттер, фаерлок и т.д.). Определяет, как именно искры будут двигаться по объекту во время сварки. **Ключевое отличие от `WeldingSparksComponent`:** - `WeldingSparksComponent` — на **инструменте** (сварке). Отвечает за то, ЧТО спавнить. @@ -204,14 +206,13 @@ public sealed partial class WeldingSparksAnimationComponent : Component **Примеры из YAML:** -| Сущность | StartingOffset | EndingOffset | Результат | -|-----------------|----------------|---------------------------|--------------------------------| -| Airlock | `0, 0.5` | *(не задан → `0, -0.5`)* | Искры идут сверху вниз | -| AirlockExternal | `-0.5, 0` | *(не задан → `0.5, 0`)* | Искры идут слева направо | -| AirlockShuttle | `-0.5, -0.2` | *(не задан → `0.5, 0.2`)* | Искры идут по диагонали | -| BaseShutter | `-0.5, -0.5` | `0.5, -0.5` | Искры идут горизонтально внизу | -| FirelockEdge | `0, -0.2` | `0, -0.5` | Искры идут вниз в нижней части | -| Walls | `0, 0` | `0, 0` | Искры никуда не идут (статичны)| +| Сущность | StartingOffset | EndingOffset | Результат | +|----------|---------------|-------------|-----------| +| Airlock (обычный шлюз) | `0, 0.5` | *(не задан → `0, -0.5`)* | Искры идут сверху вниз | +| AirlockExternal | `-0.5, 0` | *(не задан → `0.5, 0`)* | Искры идут слева направо | +| AirlockShuttle | `-0.5, -0.2` | *(не задан → `0.5, 0.2`)* | Искры идут по диагонали | +| BaseShutter | `-0.5, -0.5` | `0.5, -0.5` | Искры идут горизонтально внизу | +| FirelockEdge | `0, -0.2` | `0, -0.5` | Искры идут вниз в нижней части | --- @@ -225,7 +226,7 @@ public sealed partial class WeldingSparksAnimationComponent : Component public sealed class WeldingSparksSystem : EntitySystem ``` -НЕ наследуется от общего базового класса — сервер и клиент имеют полностью независимые системы, связанные только через сетевое событие. +НЕ наследуется от общего базового класса (в отличие от барков) — сервер и клиент имеют полностью независимые системы, связанные только через сетевое событие. #### Зависимости (Dependency Injection) @@ -254,7 +255,7 @@ public override void Initialize() 2. **`ToolDoAfterEvent` → `OnAfterUseTool()`** — обработчик завершения/отмены DoAfter. Здесь удаляется эффект. Обратите внимание: `ToolDoAfterEvent` стал `public` (был `protected`) — это одно из изменений PR, чтобы `WeldingSparksSystem` мог подписаться на него. -3. **`BeforeRangedInteractEvent` → `OnBeforeInteract()`** — хак для записи позиции клика. Нужен когда нет target-сущности. +3. **`BeforeRangedInteractEvent` → `OnBeforeInteract()`** — хак для записи позиции клика. Нужен для сварки пола, где нет target-сущности. #### OnUseTool() — обработка начала сварки @@ -284,7 +285,7 @@ private void OnUseTool(Entity ent, ref UseToolEvent args 3. **Позиция спавна.** `GetSpawnLoc()` определяет, где спавнить эффект: - Если есть `target` (и это не сам инструмент) — на координатах цели (центр шлюза). - - Если `target` нет — на последней записанной позиции клика (`LastClickLocation`), привязанной к сетке тайлов (`.SnapToGrid()`). + - Если `target` нет (сварка пола) — на последней записанной позиции клика (`LastClickLocation`), привязанной к сетке тайлов (`.SnapToGrid()`). 4. **Спавн эффекта.** Вызывает `SpawnEffect()`. @@ -309,7 +310,7 @@ private void SpawnEffect(Entity ent, ref UseToolEvent ar 2. `SpawnedEffects.Add(id, effect)` — запоминаем связь DoAfter → эффект, чтобы знать что удалить при завершении. -3. **Сетевое событие** — отправляется ТОЛЬКО если есть `target`. Если `target == null` анимация не нужна — искры просто стоят на месте до конца сварки. `GetNetEntity()` конвертирует `EntityUid` (локальный) в `NetEntity` (сетевой). +3. **Сетевое событие** — отправляется ТОЛЬКО если есть `target`. При сварке пола (`target == null`) анимация не нужна — искры просто стоят на месте до конца сварки. `GetNetEntity()` конвертирует `EntityUid` (локальный) в `NetEntity` (сетевой). #### OnAfterUseTool() — завершение/отмена сварки @@ -633,12 +634,14 @@ SubscribeLocalEvent(O **`EchoEffectWeldingSparks`** — сущность визуального эффекта. Это то, что игрок видит во время сварки. -1. `Transform` (anchored: true) - эффект привязан к позиции — не движется с физикой -2. `Sprite` - Два слоя: `smoke` (дым) и `welding_sparks` (искры, `unshaded` — не затемняются освещением). `drawdepth: Effects` — рисуется поверх большинства объектов. `snapCardinals: true` — привязка к кардинальным направлениям. `noRot: true` — спрайт не вращается -3. `AnimationPlayer` - Необходим для проигрывания покадровой анимации спрайта -4. `PointLight` - Динамический свет оранжевого цвета с радиусом 1.5 — создаёт свечение от сварки -5. `Tag: HideContextMenu` - Скрывает сущность из контекстного меню (правый клик) — игрок не может взаимодействовать с эффектом -6. `categories: HideSpawnMenu` - Скрывает из меню спавна админа +| Компонент | Назначение | +|-----------|-----------| +| `Transform` (anchored: true) | Эффект привязан к позиции — не движется с физикой | +| `Sprite` | Два слоя: `smoke` (дым) и `welding_sparks` (искры, `unshaded` — не затемняются освещением). `drawdepth: Effects` — рисуется поверх большинства объектов. `snapCardinals: true` — привязка к кардинальным направлениям. `noRot: true` — спрайт не вращается | +| `AnimationPlayer` | Необходим для проигрывания покадровой анимации спрайта | +| `PointLight` | Динамический свет оранжевого цвета с радиусом 1.5 — создаёт свечение от сварки | +| `Tag: HideContextMenu` | Скрывает сущность из контекстного меню (правый клик) — игрок не может взаимодействовать с эффектом | +| `categories: HideSpawnMenu` | Скрывает из меню спавна админа | ```yaml - type: entity @@ -664,11 +667,13 @@ SubscribeLocalEvent(O RSI-файл (Robust Sprite Image) содержит три анимации по 60 кадров каждая с интервалом 0.025 секунды: -| State | Описание | -|----------------------|--------------------------------------------------------| -| `welding_sparks` | Анимация оранжевых искр (для обычной сварки) | +| State | Описание | +|-------|----------| +| `welding_sparks` | Анимация оранжевых искр (для обычной сварки) | | `exp_welding_sparks` | Анимация бирюзовых искр (для экспериментальной сварки) | -| `smoke` | Анимация дыма (общая для обоих вариантов) | +| `smoke` | Анимация дыма (общая для обоих вариантов) | + +Лицензия: CC-BY-SA-3.0. Оригинальные спрайты от **UDaV73rus** на codebase Tau Ceti Classic ([PR #6540](https://github.com/TauCetiStation/TauCetiClassic/pull/6540)). Разделены на искры+дым **SabreML**. ### 3. Resources/Prototypes/Entities/Objects/Tools/welders.yml (ИЗМЕНЁН) @@ -693,16 +698,16 @@ RSI-файл (Robust Sprite Image) содержит три анимации по Компонент `WeldingSparksAnimation` добавлен ко всем свариваемым дверям. Каждая дверь получила свои уникальные offsets: -| Файл | Сущность | startingOffset | endingOffset | -|------------------------------|---------------------|----------------|--------------| -| `base_structureairlocks.yml` | **Airlock** | `0, 0.5` | — | -| `external.yml` | **AirlockExternal** | `-0.5, 0` | — | -| `highsec.yml` | **HighSecDoor** | `0, 0.5` | — | -| `shuttle.yml` | **AirlockShuttle** | `-0.5, -0.2` | — | -| `firelock.yml` | **BaseFirelock** | `-0.5, 0` | — | -| `firelock.yml` | **FirelockEdge** | `0, -0.2` | `0, -0.5` | -| `shutters.yml` | **BaseShutter** | `-0.5, -0.5` | `0.5, -0.5` | -| `blast_door.yml` | **BlastDoor** | `0, 0.5` | — | +| Файл | Сущность | startingOffset | endingOffset | +|------|----------|---------------|-------------| +| `base_structureairlocks.yml` | **Airlock** (и все наследники) | `0, 0.5` | — | +| `external.yml` | **AirlockExternal** | `-0.5, 0` | — | +| `highsec.yml` | **HighSecDoor** | `0, 0.5` | — | +| `shuttle.yml` | **AirlockShuttle** | `-0.5, -0.2` | — | +| `firelock.yml` | **BaseFirelock** | `-0.5, 0` | — | +| `firelock.yml` | **FirelockEdge** | `0, -0.2` | `0, -0.5` | +| `shutters.yml` | **BaseShutter** (и все наследники) | `-0.5, -0.5` | `0.5, -0.5` | +| `blast_door.yml` | **BlastDoor** | `0, 0.5` | — | **Логика выбора offsets:** - Вертикальные двери (обычные шлюзы, высокобезопасные, бласт-двери) — искры идут сверху вниз: `(0, 0.5)` → `(0, -0.5)`. @@ -766,6 +771,10 @@ RSI-файл (Robust Sprite Image) содержит три анимации по `GetOffsets()` учитывает поворот сущности в мире и поворот камеры. Для шлюзов с `SnapCardinals = true` вычисляется привязка к ближайшему кардинальному направлению. Итоговый угол поворачивает вектора start/end, чтобы искры всегда шли вдоль «шва» двери, независимо от её ориентации. Автор признаётся: «Honestly I don't understand all of RT's sprite/eye/world/cardinal rotation stuff. I just trial-and-error'd this into working.» +### Что за `pattern: Entity` в параметрах обработчиков? + +Это **Entity pattern** в SS14: `Entity` — это структура `(EntityUid Owner, T Comp)`. Когда подписываешься через `SubscribeLocalEvent`, обработчик может принимать `Entity` вместо отдельных `EntityUid` и `TComp`. Это удобнее: `ent.Owner` — UID, `ent.Comp` — компонент. Работает через implicit conversion. + ### Почему `TryGetEntity` для `SparksEnt`, но `GetEntity` для `TargetEnt`? Потому что `TargetEnt` уже проверен через `TryComp` — если компонент найден, значит сущность точно существует. А `SparksEnt` — свежеспавненная сущность, которая может ещё не полностью синхронизироваться на клиенте, поэтому используется безопасный `TryGetEntity`. 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\277\320\276 \320\263\321\200\320\260\320\261\321\203.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\277\320\276 \320\263\321\200\320\260\320\261\321\203.md" new file mode 100644 index 0000000..20e87d7 --- /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\277\320\276 \320\263\321\200\320\260\320\261\321\203.md" @@ -0,0 +1,214 @@ +# Grab System Documentation + +Документация по системе захвата (grab). + +## Оглавление +1. Суть системы +2. Архитектура и поток +3. Shared часть +4. Server часть +5. Client часть +6. Изменения в существующих файлах +7. YAML, Alerts и локализация +8. Быстрый сценарий по шагам + +## 1. Суть системы +Система захвата расширяет стандартное pulling-поведение и добавляет стадии удержания цели: +- None +- Soft +- Hard +- Choke + +Ключевые возможности: +- поэтапное усиление и ослабление захвата; +- do-after при смене стадии и попытках вырваться; +- ограничения по свободным рукам для отдельных стадий; +- влияние экипировки/компонентов на скорость перехода стадий; +- бросок цели на высоких стадиях; +- блок речи у цели на стадии Choke (сервер); +- алерты для того, кто держит, и того, кого держат. + +## 2. Архитектура и поток + +### Общий поток +1. Игрок начинает тянуть цель (обычное pulling). +2. Повторное действие по цели переводит в механику grab (повышение стадии), если игрок в combat mode. +3. При росте стадии обновляются алерты, виртуальные руки, модификаторы скорости и логика ограничения действий. +4. Цель может пытаться вырваться через do-after (количество попыток и время зависят от текущей стадии). +5. На стадии Hard/Choke возможен бросок, который запускает отдельную коллизионную логику (GrabThrownComponent). +6. При сбросе стадии до None или остановке pulling состояние очищается. + +### Ключевые точки интеграции +- База pulling: Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +- Grab-логика: Content.Shared/_ECHO/Grab/Systems/PullingSystem.Grab.cs +- Серверные эффекты (попапы, speech block, throw): Content.Server/_ECHO/Grab/PullingSystem.cs + +## 3. Shared часть + +### 3.1 Основные сущности и данные + +#### PullerComponent +Файл: Content.Shared/Movement/Pulling/Components/PullerComponent.cs + +Добавляет/использует: +- GrabStage Stage +- Dictionary GrabStats +- TimeSpan NextStageChange +- DoAfterId? StageIncreaseDoAfter +- int GrabbingDirection +- List VirtualItems +- ProtoId PullingAlert (ECHOPulling) + +Стадии enum: +- None = 0 +- Soft = 1 +- Hard = 2 +- Choke = 3 + +#### PullableComponent +Файл: Content.Shared/Movement/Pulling/Components/PullableComponent.cs + +Добавляет/использует: +- ProtoId PulledAlert (ECHOPulled) +- TimeSpan LastEscapeAttempt +- int EscapeAttemptCounter +- DoAfterId? EscapeAttemptDoAfter + +#### GrabStageStats +Файл: Content.Shared/_ECHO/Grab/GrabStageStats.cs + +Параметры стадии: +- RequiredHands +- DoaftersToEscape +- MovementSpeedModifier +- EscapeAttemptTime +- SetStageTime + +### 3.2 События + +Файлы: +- Content.Shared/_ECHO/Grab/Events/GrabStageChangedEvent.cs +- Content.Shared/_ECHO/Grab/Events/SetGrabStageDoAfterEvent.cs +- Content.Shared/_ECHO/Grab/Events/GrabEscapeDoAfterEvent.cs +- Content.Shared/_ECHO/Grab/Events/ModifyGrabStageTimeEvent.cs + +Назначение: +- GrabStageChangedEvent: синхронизация реакции на смену стадии (алерты, руки, блоки и т.д.). +- SetGrabStageDoAfterEvent: завершение do-after при изменении стадии. +- GrabEscapeDoAfterEvent: завершение do-after попытки вырваться. +- ModifyGrabStageTimeEvent: модификация тайминга перехода стадии, поддерживает relay через инвентарь (IInventoryRelayEvent). + +### 3.3 Компоненты-модификаторы + +#### ModifyGrabStageTimeComponent +Файл: Content.Shared/_ECHO/Grab/Components/ModifyGrabStageTimeComponent.cs + +Хранит словарь множителей тайминга по стадиям: +- Dictionary Modifiers + +#### GrabProtectionComponent + GrabProtectionSystem +Файлы: +- Content.Shared/_ECHO/GrabProtection/GrabProtectionComponent.cs +- Content.Shared/_ECHO/GrabProtection/GrabProtectionSystem.cs + +Поведение: +- На ModifyGrabStageTimeEvent выставляет Cancelled = true. +- Фактически запрещает прогрессию захвата для сущностей с этим компонентом. + +### 3.4 Базовая логика в PullingSystem.Grab + +Файл: Content.Shared/_ECHO/Grab/Systems/PullingSystem.Grab.cs + +Ключевые методы: +- TryStartPullingOrGrab +- TryIncreaseGrabStageOrStopPulling +- TryLowerGrabStageOrStopPulling +- TryStartGrabDoAfter +- TryIncreaseGrabStage +- TryLowerGrabStage +- TryEscapeFromGrab +- Throw (shared-base) + +Особенности: +- Проверка hands и виртуальных предметов для стадий, где нужно больше рук. +- Гейт по NextStageChange (кулдаун смены стадии). +- Поддержка модификатора времени через ModifyGrabStageTimeEvent. +- OnThrownDoHit: обработка попаданий при броске и доп. эффекты (стан/урон/down). +- OnBeforeClimb: логика forced bonk при определенных условиях. + +### 3.5 Изменения в базовом PullingSystem + +Файл: Content.Shared/Movement/Pulling/Systems/PullingSystem.cs + +Интеграция с grab: +- InitializeGrab вызывается в Initialize. +- OnStopPullingAlert снижает стадию или останавливает pull. +- OnStopBeingPulledAlert запускает попытку вырваться. +- OnPullableMoveInput может инициировать escape attempt. +- OnReleasePulledObject учитывает grab-сценарий. +- StopPulling очищает grab-состояние (stage, do-afters, virtual items, cooldown). +- RefreshMovementSpeedModifiers учитывает стадию и направление изменения стадии. + +## 4. Server часть + +Файл: Content.Server/_ECHO/Grab/PullingSystem.cs +Класс: ServerPullingSystem : PullingSystem + +Что добавлено на сервере: +- Popups и звуки при повышении/понижении стадии. +- Реальное изменение Stage при успешном base-вызове. +- Блок речи цели на стадии Choke (если нет IgnoreChokeSpeechBlocking тега). +- Серверный бросок цели и откатный импульс для атакующего. +- Admin-логи для действий grab. + +## 5. Client часть +На текущей реализации отдельной клиентской grab-системы не добавлено. +Клиент получает состояние через networked поля компонентов и стандартные события/алерты. + +## 6. Изменения в существующих файлах + +Ключевые существующие файлы, куда интегрирован grab: +- Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +- Content.Shared/Movement/Pulling/Components/PullerComponent.cs +- Content.Shared/Movement/Pulling/Components/PullableComponent.cs +- Content.Shared/Inventory/InventorySystem.Relay.cs + +Ключевые новые/выделенные grab-файлы: +- Content.Shared/_ECHO/Grab/Systems/PullingSystem.Grab.cs +- Content.Shared/_ECHO/Grab/GrabStageStats.cs +- Content.Shared/_ECHO/Grab/Events/*.cs +- Content.Shared/_ECHO/Grab/Components/*.cs +- Content.Shared/_ECHO/GrabProtection/*.cs +- Content.Server/_ECHO/Grab/PullingSystem.cs + +## 7. YAML, Alerts и локализация + +### Alerts +Файл: +- Resources/Prototypes/_ECHO/Alerts/alerts.yml + +Используемые alert id: +- ECHOPulled (для цели) +- ECHOPulling (для атакующего) + +### Локализация +Файлы: +- Resources/Locale/ru-RU/_ECHO/grab/popups.ftl +- Resources/Locale/ru-RU/alerts/alerts.ftl +- Resources/Locale/en-US/alerts/alerts.ftl + +Используются строки: +- grab-increase-...-popup-... +- grab-lower-...-popup-... +- grab-escape-...-popup-... +- grab-speech-attempt-choke +- forced-bonkable-success-message +- alerts-pulled-name / alerts-pulling-name + +## 8. Быстрый сценарий по шагам +1. Начать pull цели. +2. В боевом режиме повысить стадию до Soft/Hard/Choke. +3. На Choke цель теряет возможность говорить (server-side check). +4. Цель может жать release и запускать escape do-after. +5. При успехе escape стадия у атакующего снижается. +6. При остановке pull вся grab-состояние и временные сущности очищаются. \ No newline at end of file