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" deleted file mode 100644 index af3967d..0000000 --- "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" +++ /dev/null @@ -1,798 +0,0 @@ -# Система анимации искр сварки (Welding Sparks Animation) — полная документация - -
- -Данный документ — сводка информации по новым файлам, системам/компонентам и изменениям в существующих файлах связанным с системой анимации искр сварки (Welding Sparks Animation). - ---- - -## Оглавление - -1. [Общая суть системы](#общая-суть-системы) -2. [Архитектура: как всё связано](#архитектура-как-всё-связано) -3. [Новые файлы — Ядро системы (Shared)](#новые-файлы--ядро-системы-shared) -4. [Новые файлы — Сервер](#новые-файлы--сервер) -5. [Новые файлы — Клиент](#новые-файлы--клиент) -6. [Изменения в существующих файлах](#изменения-в-существующих-файлах) -7. [YAML-прототипы и ресурсы](#yaml-прототипы-и-ресурсы) -8. [Путь данных: от нажатия кнопки до искр на экране](#путь-данных-от-нажатия-кнопки-до-искр-на-экране) -9. [Частые вопросы для новых кодеров](#частые-вопросы-для-новых-кодеров) - ---- - -## Общая суть системы - -Когда игрок использует сварочный аппарат (Welder) на объекте (например, заваривает шлюз), система спавнит визуальный эффект искр и дыма в месте сварки. Если у свариваемого объекта настроена анимация — искры плавно перемещаются по его поверхности за время сварки, создавая иллюзию движения сварочного шва. - -Как это работает на пальцах: -1. Игрок берёт сварку и кликает на объект (например, шлюз). -2. Система инструментов (`SharedToolSystem`) запускает DoAfter-таймер и вызывает новое событие `UseToolEvent` на инструменте. -3. Серверная система `WeldingSparksSystem` ловит это событие: спавнит сущность-эффект (искры + дым) в позиции цели, а также играет звук сварки. -4. Сервер отправляет клиентам сетевое событие `SpawnedWeldingSparksEvent` с информацией о цели, эффекте и длительности. -5. Клиентская система `WeldingSparksAnimationSystem` получает событие и запускает анимацию: плавно перемещает спрайт эффекта от `StartingOffset` к `EndingOffset` на свариваемом объекте. -6. Когда DoAfter завершается (или отменяется) — сервер удаляет сущность-эффект. - ---- - -## Архитектура - -``` -Схема потока данных: - -[Игрок кликает сваркой на шлюз] - │ - ▼ -[Shared: SharedToolSystem.UseTool()] - ├── Запускает DoAfter-таймер - └── Вызывает UseToolEvent на инструменте (сварке) - │ - ▼ -[Сервер: WeldingSparksSystem.OnUseTool()] - ├── Играет звук сварки - ├── Определяет позицию спавна эффекта (координаты цели или клика) - ├── Spawn("EchoEffectWeldingSparks") — создаёт сущность-эффект - └── RaiseNetworkEvent(SpawnedWeldingSparksEvent) → все клиенты - │ - ▼ -[Клиент: WeldingSparksAnimationSystem.OnSpawnedWeldingSparks()] - ├── Проверяет WeldableComponent + WeldingSparksAnimationComponent на цели - ├── Вычисляет начальный и конечный Offset (с учётом поворота и типа действия) - └── Запускает Animation (линейная интерполяция SpriteComponent.Offset) - │ - ▼ -[DoAfter завершился / отменён] - │ - ▼ -[Сервер: WeldingSparksSystem.OnAfterUseTool()] - └── QueueDel(effect) — удаляет сущность-эффект -``` - ---- - -## Новые файлы — Ядро системы (Shared) - -### 1. Content.Shared/_ECHO/Tools/UseToolEvent.cs - -```csharp -public readonly record struct UseToolEvent -{ - public readonly EntityUid User; - public readonly EntityUid? Target; - public readonly ushort DoAfterIdx; - public readonly TimeSpan DoAfterLength; - - public UseToolEvent(EntityUid user, EntityUid? target, ushort doAfterIdx, TimeSpan doAfterLength) - { - User = user; - Target = target; - DoAfterIdx = doAfterIdx; - DoAfterLength = doAfterLength; - } -} -``` - -**Что это:** Событие, которое поднимается на инструменте (сварке) после того, как 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)`), который используется как ключ словаря для отслеживания спавненных эффектов. -- `DoAfterLength` — длительность DoAfter-таймера. Передаётся клиенту, чтобы анимация длилась ровно столько же, сколько сварка. - -**Почему `ushort DoAfterIdx`, а не полный `DoAfterId`?** -Комментарий в коде объясняет: ``Ideally this would just be a DoAfterId instance and wouldn't need converting back, but this is in '.Common' so oh well.`` — тип `DoAfterId` включает в себя `EntityUid`, который не доступен в Shared-контексте как нужный полный тип. Поэтому передаётся только индекс, а полный ID восстанавливается на сервере: `new DoAfterId(args.User, args.DoAfterIdx)`. - ---- - -### 2. Content.Shared/_ECHO/Tools/SpawnedWeldingSparksEvent.cs - -```csharp -[Serializable, NetSerializable] -public sealed partial class SpawnedWeldingSparksEvent(NetEntity targetEnt, NetEntity sparksEnt, TimeSpan duration) : EntityEventArgs -{ - public NetEntity TargetEnt = targetEnt; - public NetEntity SparksEnt = sparksEnt; - public TimeSpan Duration = duration; -} -``` - -**Что это:** Сетевое событие, которое сервер отправляет клиентам после спавна эффекта искр. Клиент использует его для запуска анимации движения. - -**Атрибуты:** -- `[Serializable, NetSerializable]` — ОБЯЗАТЕЛЬНО для любого события, отправляемого по сети. Без этого движок не сможет сериализовать объект для передачи. -- `EntityEventArgs` — базовый класс для сетевых событий в SS14. -- Используется **primary constructor** синтаксис C# 12: параметры конструктора объявлены прямо в определении класса `(NetEntity targetEnt, NetEntity sparksEnt, TimeSpan duration)`. - -**Поля:** -- `TargetEnt` — `NetEntity` (сетевой ID) свариваемой сущности (шлюз, стена и т.д.). Тип `NetEntity` — это ID сущности в формате, понятном и серверу и клиенту (в отличие от обычного `EntityUid`, который локален для каждой стороны). -- `SparksEnt` — `NetEntity` спавненной сущности-эффекта (искры + дым). Клиент будет анимировать именно её спрайт. -- `Duration` — длительность анимации. Совпадает с длительностью DoAfter, чтобы анимация шла ровно столько, сколько длится сварка. - -**Путь события:** `Сервер (RaiseNetworkEvent)` → `Сеть` → `Клиент (SubscribeNetworkEvent)` - ---- - -### 3. Content.Shared/_ECHO/Tools/Components/WeldingSparksComponent.cs - -```csharp -[RegisterComponent] -public sealed partial class WeldingSparksComponent : Component -{ - [DataField("effect")] - public EntProtoId EffectProto = "EchoEffectWeldingSparks"; - - [ViewVariables(VVAccess.ReadOnly)] - public Dictionary SpawnedEffects = []; - - public EntityCoordinates? LastClickLocation; -} -``` - -**Что это:** Компонент, который прикрепляется к сварочному инструменту. Определяет, какой эффект искр спавнить, и хранит состояние активных эффектов. - -**Атрибуты:** -- `[RegisterComponent]` — регистрирует компонент в системе, чтобы его можно было добавлять к сущностям через YAML-прототипы. -- `[DataField("effect")]` — позволяет задать значение из YAML. `"effect"` — имя поля в YAML (а не автоматическое из C#-имени `EffectProto`). -- `[ViewVariables(VVAccess.ReadOnly)]` — позволяет просматривать словарь активных эффектов в отладочном инструменте View Variables (VV), но не менять. - -**Поля:** -- `EffectProto` — ID прототипа сущности-эффекта. По умолчанию `"EchoEffectWeldingSparks"` (обычные оранжевые искры). Для экспериментальной сварки переопределяется на `"EchoEffectWeldingSparksExp"` (бирюзовые искры). Тип `EntProtoId` — строка-обёртка, которая «знает», что это ID прототипа сущности. -- `SpawnedEffects` — словарь `Dictionary`, связывающий каждый активный DoAfter с его эффектом. Когда DoAfter завершается или отменяется, система удаляет соответствующий эффект из этого словаря и из мира. -- `LastClickLocation` — последняя позиция клика игрока. Нужна для кейса сварки пола (floor tiles): при сварке пола нет конкретной сущности-цели (`target == null`), поэтому эффект спавнится на тайле, куда кликнул игрок. Записывается через событие `BeforeRangedInteractEvent`. - -**Где добавляется:** В YAML-прототипе сварочного инструмента: -```yaml -# welders.yml (базовый Welder) -- type: WeldingSparks - -# Экспериментальная сварка — другой эффект -- type: WeldingSparks - effect: EchoEffectWeldingSparksExp -``` - ---- - -### 4. Content.Shared/_ECHO/Tools/Components/WeldingSparksAnimationComponent.cs - -```csharp -[RegisterComponent] -public sealed partial class WeldingSparksAnimationComponent : Component -{ - [DataField(required: true)] - public Vector2 StartingOffset; - - [DataField] - public Vector2? EndingOffset; -} -``` - -**Что это:** Компонент, который прикрепляется к **свариваемой** сущности (шлюз, шаттер, фаерлок и т.д.). Определяет, как именно искры будут двигаться по объекту во время сварки. - -**Ключевое отличие от `WeldingSparksComponent`:** -- `WeldingSparksComponent` — на **инструменте** (сварке). Отвечает за то, ЧТО спавнить. -- `WeldingSparksAnimationComponent` — на **цели** (шлюзе). Отвечает за то, КАК анимировать. - -**Атрибуты:** -- `[DataField(required: true)]` — `StartingOffset` обязателен в YAML. Без него прототип не загрузится. -- `[DataField]` — `EndingOffset` опционален. - -**Поля:** -- `StartingOffset` — `Vector2` — смещение спрайта эффекта в начале анимации (в тайлах). Например, `(0, 0.5)` означает «начать на полтайла выше центра сущности». Это задаёт стартовую точку сварочного шва. -- `EndingOffset` — `Vector2?` (nullable) — смещение в конце анимации. Если **не задано** (`null`), анимация автоматически идёт в противоположную сторону от `StartingOffset` — то есть к `-StartingOffset`. Это удобно: для симметричных объектов (вроде обычных шлюзов) достаточно задать только начало, а конец вычислится автоматически (сверху вниз). - -**Примеры из 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` | Искры идут вниз в нижней части | - ---- - -## Новые файлы — Сервер - -### 1. Content.Server/_ECHO/Tools/WeldingSparksSystem.cs - -Это **главный серверный файл** системы. Через него проходит каждое использование сварки. - -```csharp -public sealed class WeldingSparksSystem : EntitySystem -``` - -НЕ наследуется от общего базового класса (в отличие от барков) — сервер и клиент имеют полностью независимые системы, связанные только через сетевое событие. - -#### Зависимости (Dependency Injection) - -```csharp -[Dependency] private readonly ToolSystem _toolSystem = default!; -``` - -- `_toolSystem` — доступ к системе инструментов. Используется для проигрывания звука сварки через `PlayToolSound()`. - -#### Initialize() — подписка на события - -```csharp -public override void Initialize() -{ - base.Initialize(); - - SubscribeLocalEvent(OnUseTool); - SubscribeLocalEvent(OnAfterUseTool); - SubscribeLocalEvent(OnBeforeInteract); -} -``` - -Три подписки: - -1. **`UseToolEvent` → `OnUseTool()`** — главный обработчик. Вызывается когда инструмент начал использоваться (DoAfter стартовал). Здесь спавнится эффект и отправляется сетевое событие. - -2. **`ToolDoAfterEvent` → `OnAfterUseTool()`** — обработчик завершения/отмены DoAfter. Здесь удаляется эффект. Обратите внимание: `ToolDoAfterEvent` стал `public` (был `protected`) — это одно из изменений PR, чтобы `WeldingSparksSystem` мог подписаться на него. - -3. **`BeforeRangedInteractEvent` → `OnBeforeInteract()`** — хак для записи позиции клика. Нужен для сварки пола, где нет target-сущности. - -#### OnUseTool() — обработка начала сварки - -```csharp -private void OnUseTool(Entity ent, ref UseToolEvent args) -{ - if (TryComp(ent, out var toolComp)) - { - _toolSystem.PlayToolSound(ent, toolComp, null, AudioParams.Default.AddVolume(-2f)); - } - - var doAfterId = new DoAfterId(args.User, args.DoAfterIdx); - - var spawnLoc = GetSpawnLoc(ent, args.Target); - if (spawnLoc is not { } loc) - return; - - SpawnEffect(ent, ref args, doAfterId, loc); -} -``` - -Пошагово: - -1. **Звук сварки.** Проверяем, есть ли `ToolComponent` на инструменте. Если есть — проигрываем звук сварки, но на 2 дБ тише стандартного (`AddVolume(-2f)`). Параметр `null` вместо `user` — звук не будет «предсказываться» клиентом (predictable = false у сварки), а `AudioParams` — новый параметр, добавленный этим PR в `PlayToolSound()`. - -2. **DoAfterId.** Восстанавливаем полный `DoAfterId` из `User` + `DoAfterIdx`. Этот ID будет ключом в словаре `SpawnedEffects`, чтобы потом найти и удалить нужный эффект. - -3. **Позиция спавна.** `GetSpawnLoc()` определяет, где спавнить эффект: - - Если есть `target` (и это не сам инструмент) — на координатах цели (центр шлюза). - - Если `target` нет (сварка пола) — на последней записанной позиции клика (`LastClickLocation`), привязанной к сетке тайлов (`.SnapToGrid()`). - -4. **Спавн эффекта.** Вызывает `SpawnEffect()`. - -#### SpawnEffect() — создание эффекта - -```csharp -private void SpawnEffect(Entity ent, ref UseToolEvent args, DoAfterId id, EntityCoordinates spawnLoc) -{ - var effect = Spawn(ent.Comp.EffectProto, spawnLoc); - ent.Comp.SpawnedEffects.Add(id, effect); - - if (args.Target is { } target) - { - RaiseNetworkEvent(new SpawnedWeldingSparksEvent(GetNetEntity(target), GetNetEntity(effect), args.DoAfterLength)); - } -} -``` - -Пошагово: - -1. `Spawn(ent.Comp.EffectProto, spawnLoc)` — создаёт сущность `EchoEffectWeldingSparks` (или `EchoEffectWeldingSparksExp` для экспериментальной сварки) на нужных координатах. - -2. `SpawnedEffects.Add(id, effect)` — запоминаем связь DoAfter → эффект, чтобы знать что удалить при завершении. - -3. **Сетевое событие** — отправляется ТОЛЬКО если есть `target`. При сварке пола (`target == null`) анимация не нужна — искры просто стоят на месте до конца сварки. `GetNetEntity()` конвертирует `EntityUid` (локальный) в `NetEntity` (сетевой). - -#### OnAfterUseTool() — завершение/отмена сварки - -```csharp -private void OnAfterUseTool(Entity ent, ref SharedToolSystem.ToolDoAfterEvent args) -{ - if (!ent.Comp.SpawnedEffects.TryGetValue(args.DoAfter.Id, out var effect)) - return; - - QueueDel(effect); - ent.Comp.SpawnedEffects.Remove(args.DoAfter.Id); -} -``` - -Когда DoAfter завершается (успешно или отменённый): -1. Ищем эффект по ID завершившегося DoAfter в словаре. -2. `QueueDel(effect)` — ставим сущность-эффект в очередь на удаление (не удаляем мгновенно — это безопаснее). -3. Убираем запись из словаря. - -**Важно:** Этот обработчик вызывается при ЛЮБОМ завершении DoAfter — и при успешной сварке, и при отмене (игрок двинулся, получил урон). В обоих случаях эффект нужно убрать. - -#### OnBeforeInteract() — запись позиции клика - -```csharp -private void OnBeforeInteract(Entity ent, ref BeforeRangedInteractEvent args) -{ - if (args.CanReach) - ent.Comp.LastClickLocation = args.ClickLocation; -} -``` - -**Зачем?** Комментарий в коде объясняет: ``This is a pretty hacky way of putting the spark effect in the right spot when welding a floor tile, since that doesn't pass a target arg.`` - -При сварке пола система инструментов не передаёт конкретный `target` (тайл — не сущность). Поэтому запоминаем координаты клика **до** начала использования инструмента. `args.CanReach` — проверка, что игрок достаёт до места клика (позже `IsValid()` проверяется в `GetSpawnLoc()`). - ---- - -## Новые файлы — Клиент - -### 1. Content.Client/_ECHO/Tools/WeldingSparksAnimationSystem.cs - -Клиентская система, которая **проигрывает анимацию движения искр**. - -```csharp -public sealed class WeldingSparksAnimationSystem : EntitySystem -``` - -#### Зависимости (Dependency Injection) - -```csharp -[Dependency] private readonly AnimationPlayerSystem _animation = default!; -[Dependency] private readonly IEyeManager _eyeManager = default!; -[Dependency] private readonly TransformSystem _transformSystem = default!; -``` - -- `_animation` — система анимаций движка Robust Toolbox. Управляет воспроизведением анимационных треков. -- `_eyeManager` — менеджер камеры. Нужен для расчёта поворота (чтобы искры корректно двигались при повёрнутой камере). -- `_transformSystem` — система трансформов. Нужна для получения мирового поворота сущности. - -#### Initialize() — подписка - -```csharp -public override void Initialize() -{ - base.Initialize(); - SubscribeNetworkEvent(OnSpawnedWeldingSparks); -} -``` - -Слушаем сетевое событие `SpawnedWeldingSparksEvent` от сервера. - -#### OnSpawnedWeldingSparks() — запуск анимации - -```csharp -private void OnSpawnedWeldingSparks(SpawnedWeldingSparksEvent ev) -{ - var targetEnt = GetEntity(ev.TargetEnt); - if (!TryComp(targetEnt, out var weldableComp) - || !TryComp(targetEnt, out var sparksAnim)) - return; - - if (!TryGetEntity(ev.SparksEnt, out var sparksEnt)) - return; - - var animationPlayer = EnsureComp(targetEnt); - if (_animation.HasRunningAnimation(targetEnt, animationPlayer, ANIM_KEY)) - return; - - var (startOffset, endOffset) = GetOffsets((targetEnt, sparksAnim), weldableComp.IsWelded); - - var animation = new Animation() - { - Length = ev.Duration, - AnimationTracks = - { - new AnimationTrackComponentProperty() - { - ComponentType = typeof(SpriteComponent), - Property = nameof(SpriteComponent.Offset), - InterpolationMode = AnimationInterpolationMode.Linear, - KeyFrames = - { - new AnimationTrackProperty.KeyFrame(startOffset, 0f), - new AnimationTrackProperty.KeyFrame(endOffset, (float) ev.Duration.TotalSeconds), - } - } - } - }; - - _animation.Play(sparksEnt.Value, animation, ANIM_KEY); -} -``` - -Пошагово: - -1. **Валидация цели.** `GetEntity(ev.TargetEnt)` — конвертирует `NetEntity` (сетевой ID) в `EntityUid` (локальный ID). Проверяем, что у цели есть: - - `WeldableComponent` — компонент, позволяющий заваривать/развареивать (стандартный SS14). - - `WeldingSparksAnimationComponent` — наш новый компонент с настройками анимации. - Если чего-то нет — выходим (эффект будет просто стоять на месте, без анимации). - -2. **Валидация эффекта.** `TryGetEntity(ev.SparksEnt, out var sparksEnt)` — безопасная конвертация. Если сущность-эффект ещё не создана на клиенте — выходим. - -3. **Защита от дублирования.** `EnsureComp` добавляет компонент анимации если его нет. `HasRunningAnimation` — если анимация уже запущена (повторный вызов), не запускаем новую. - -4. **Расчёт смещений.** `GetOffsets()` — ключевой метод. Возвращает начальное и конечное смещение с учётом: - - Поворота сущности. - - Поворота камеры. - - Направления сварки (заваривание / разваривание). - -5. **Создание анимации.** Объект `Animation` с одним треком: - - `AnimationTrackComponentProperty` — анимирует свойство компонента. - - `ComponentType = typeof(SpriteComponent)` — анимируем спрайт. - - `Property = nameof(SpriteComponent.Offset)` — конкретно свойство Offset (смещение). - - `InterpolationMode = Linear` — линейная интерполяция (равномерное движение). - - Два ключевых кадра (KeyFrames): начальный Offset в момент `0` и конечный Offset в момент `Duration`. - -6. **Запуск.** `_animation.Play(sparksEnt.Value, animation, ANIM_KEY)` — передаём анимацию плееру. `ANIM_KEY = "WeldAnim"` — уникальный ключ, чтобы избежать конфликтов с другими анимациями. - -#### GetOffsets() — расчёт направления анимации - -```csharp -private (Vector2, Vector2) GetOffsets(Entity ent, bool isWelded) -{ - var start = ent.Comp.StartingOffset; - var end = ent.Comp.EndingOffset ?? -ent.Comp.StartingOffset; - - // Rotation - if (TryComp(ent, out var sprite)) - { - var worldRotation = _transformSystem.GetWorldRotation(ent); - var eyeRotation = _eyeManager.CurrentEye.Rotation; - var relativeRotation = (worldRotation + eyeRotation).Reduced().FlipPositive(); - var cardinalSnapping = sprite.SnapCardinals - ? relativeRotation.GetCardinalDir().ToAngle() - : Angle.Zero; - var finalAngle = sprite.NoRotation - ? relativeRotation - : relativeRotation - cardinalSnapping; - - start = finalAngle.RotateVec(start); - end = finalAngle.RotateVec(end); - } - - // Welding direction - if (!isWelded) - return (start, end); // Заваривание: вперёд - else - return (end, start); // Разваривание: назад -} -``` - -Пошагово: - -1. **Базовые смещения.** Берём `StartingOffset` и `EndingOffset` из компонента. Если `EndingOffset` не задан — используем `-StartingOffset` (противоположная точка). - -2. **Расчёт поворота.** Самая сложная часть. Комментарий автора: - > *Honestly I don't understand all of RT's sprite/eye/world/cardinal rotation stuff. I just trial-and-error'd this into working. (why isn't there a helper function for this) :(* - - Логика: - - `worldRotation` — как повёрнута сущность в мире. - - `eyeRotation` — как повёрнута камера игрока. - - `relativeRotation` — итоговый относительный поворот. `.Reduced()` нормализует угол, `.FlipPositive()` делает его положительным. - - `cardinalSnapping` — если спрайт привязан к кардинальным направлениям (`SnapCardinals = true`, что обычно для шлюзов), вычисляем привязку к ближайшему кардинальному направлению. - - `finalAngle` — итоговый угол поворота. Если спрайт не вращается (`NoRotation = true`), берём полный относительный поворот. Иначе — вычитаем кардинальную привязку. - - `RotateVec()` — поворачивает вектор на этот угол. Содержит внутреннюю проверку `Theta == 0`, так что не нужно оптимизировать вручную. - -3. **Направление сварки.** - - При **заваривании** (`isWelded == false` — объект ещё не заварен) — искры идут от `start` к `end` (стандартное направление). - - При **разваривании** (`isWelded == true` — объект уже заварен, снимаем сварку) — искры идут **в обратном направлении**: от `end` к `start`. Визуально это выглядит так, будто шов «расходится». - ---- - -## Изменения в существующих файлах - -### 1. Content.Shared/Tools/Systems/SharedToolSystem.cs - -Этот файл содержит основную логику использования инструментов. PR вносит несколько точечных изменений: - -#### Изменение 1: Добавлены using-директивы - -```csharp -using Content.Shared._ECHO.Tools; // Echo-Tweak "Wielding Sparks animation" -using Robust.Shared.Audio; // Echo-Tweak "Wielding Sparks animation" -``` - -`Content.Shared._ECHO.Tools` нужен для типа `UseToolEvent`. `Robust.Shared.Audio` — для типа `AudioParams` (ранее не использовался в этом файле напрямую). - -#### Изменение 2: PlayToolSound() — добавлен параметр audioParams - -**Было:** -```csharp -public void PlayToolSound(EntityUid uid, ToolComponent tool, EntityUid? user) -{ - if (tool.UseSound == null) - return; - _audioSystem.PlayPredicted(tool.UseSound, uid, user); -} -``` - -**Стало:** -```csharp -public void PlayToolSound(EntityUid uid, ToolComponent tool, EntityUid? user, AudioParams? audioParams = null) -{ - if (tool.UseSound == null) - return; - _audioSystem.PlayPredicted(tool.UseSound, uid, user, audioParams); -} -``` - -Добавлен опциональный параметр `AudioParams? audioParams = null`. Это позволяет вызывающему коду передать свои параметры аудио (например, пониженную громкость), при этом не ломая существующий код (параметр имеет значение по умолчанию `null`). - -`WeldingSparksSystem` использует это для проигрывания звука сварки чуть тише: `AudioParams.Default.AddVolume(-2f)`. - -#### Изменение 3: UseTool() — разделение переменной и добавление UseToolEvent - -**Было:** -```csharp -var toolEvent = new ToolDoAfterEvent(fuel, doAfterEv, GetNetEntity(target)); -var doAfterArgs = new DoAfterArgs(EntityManager, user, delay / toolComponent.SpeedModifier, toolEvent, tool, target: target, used: tool) -{ - // ... -}; - -_doAfterSystem.TryStartDoAfter(doAfterArgs, out id); -return true; -``` - -**Стало:** -```csharp -var toolEvent = new ToolDoAfterEvent(fuel, doAfterEv, GetNetEntity(target)); -var doAfterLength = delay / toolComponent.SpeedModifier; -var doAfterArgs = new DoAfterArgs(EntityManager, user, doAfterLength, toolEvent, tool, target: target, used: tool) -{ - // ... -}; - -if (_doAfterSystem.TryStartDoAfter(doAfterArgs, out id)) -{ - RaiseLocalEvent(tool, new UseToolEvent(user, target, id.Value.Index, doAfterLength)); -} -return true; -``` - -Два ключевых изменения: - -1. **Вынос `doAfterLength` в отдельную переменную.** `delay / toolComponent.SpeedModifier` вычисляется один раз и используется и в `DoAfterArgs`, и в `UseToolEvent`. Раньше это выражение было инлайн в конструкторе `DoAfterArgs`. - -2. **Событие `UseToolEvent`.** `TryStartDoAfter` теперь обёрнут в `if` — если DoAfter успешно стартовал, поднимается `UseToolEvent` на инструменте. Это позволяет `WeldingSparksSystem` (и любой другой модульной системе) узнать, что инструмент начал работу, получив `target`, `doAfterId` и `doAfterLength`. - -#### Изменение 4: ToolDoAfterEvent — модификатор доступа - -**Было:** -```csharp -[Serializable, NetSerializable] -protected sealed partial class ToolDoAfterEvent : DoAfterEvent -``` - -**Стало:** -```csharp -[Serializable, NetSerializable] -public sealed partial class ToolDoAfterEvent : DoAfterEvent -``` - -`protected` → `public`. Это необходимо чтобы `WeldingSparksSystem` (который находится в другом namespace) мог подписаться на событие `ToolDoAfterEvent`: -```csharp -SubscribeLocalEvent(OnAfterUseTool); -``` - -С `protected` это было бы невозможно — событие было бы видно только внутри `SharedToolSystem` и его наследников. - ---- - -## YAML-прототипы и ресурсы - -### 1. Resources/Prototypes/_ECHO/Entities/Effects/welding_sparks.yml (НОВЫЙ) - -```yaml -- type: entity - id: EchoEffectWeldingSparks - categories: [ HideSpawnMenu ] - components: - - type: Transform - anchored: true - - type: Sprite - snapCardinals: true - noRot: true - drawdepth: Effects - sprite: /Textures/_ECHO/Effects/welding_effect.rsi - layers: - - state: smoke - - state: welding_sparks - shader: unshaded - - type: AnimationPlayer - - type: PointLight - color: orange - radius: 1.5 - - type: Tag - tags: - - HideContextMenu -``` - -**`EchoEffectWeldingSparks`** — сущность визуального эффекта. Это то, что игрок видит во время сварки. - -| Компонент | Назначение | -|-----------|-----------| -| `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 - parent: EchoEffectWeldingSparks - id: EchoEffectWeldingSparksExp - categories: [ HideSpawnMenu ] - components: - - type: Sprite - layers: - - state: smoke - - state: exp_welding_sparks - shader: unshaded - - type: PointLight - color: MediumAquamarine -``` - -**`EchoEffectWeldingSparksExp`** — вариант для экспериментальной сварки. -- Наследуется от `EchoEffectWeldingSparks` (через `parent`). -- Использует другой state спрайта: `exp_welding_sparks` вместо `welding_sparks`. -- Свет бирюзового цвета (`MediumAquamarine`) вместо оранжевого — совпадает с цветом пламени экспериментальной сварки (`color: lightblue` в `PointLight` сварки). - -### 2. Resources/Textures/_ECHO/Effects/welding_effect.rsi/ (НОВЫЙ) - -RSI-файл (Robust Sprite Image) содержит три анимации по 60 кадров каждая с интервалом 0.025 секунды: - -| State | Описание | -|-------|----------| -| `welding_sparks` | Анимация оранжевых искр (для обычной сварки) | -| `exp_welding_sparks` | Анимация бирюзовых искр (для экспериментальной сварки) | -| `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 (ИЗМЕНЁН) - -Компонент `WeldingSparks` добавлен к сварочным инструментам: - -**Базовый Welder:** -```yaml - - type: WeldingSparks #PE-Tweak "Wielding Sparks Animation" -``` -Использует эффект по умолчанию (`EchoEffectWeldingSparks`). - -**WelderExperimental (экспериментальная сварка):** -```yaml - - type: WeldingSparks # PE-Tweak "Wielding Sparks Animation" - effect: EchoEffectWeldingSparksExp -``` -Переопределяет эффект на бирюзовый вариант. - -Все остальные сварки (`WelderIndustrial`, `WelderIndustrialAdvanced`, `WelderMini`) наследуют от `Welder`, поэтому получают `WeldingSparks` автоматически. - -### 4. Изменения в YAML-прототипах дверей (ИЗМЕНЕНЫ) - -Компонент `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` | — | - -**Логика выбора offsets:** -- Вертикальные двери (обычные шлюзы, высокобезопасные, бласт-двери) — искры идут сверху вниз: `(0, 0.5)` → `(0, -0.5)`. -- Горизонтальные двери (внешние шлюзы, фаерлоки) — искры идут слева направо: `(-0.5, 0)` → `(0.5, 0)`. -- Шаттерсы — искры идут горизонтально в нижней части: `(-0.5, -0.5)` → `(0.5, -0.5)` (c явным `endingOffset`). -- Шлюз шаттла — по диагонали с учётом расположения стыковочного порта. -- Граничный фаерлок (`FirelockEdge`) — короткое вертикальное движение в нижней части спрайта. - ---- - -## Путь данных: от нажатия кнопки до искр на экране - -``` -1. Игрок кликает сваркой на шлюз - │ -2. SharedToolSystem.UseTool() проверяет качества инструмента, - запускает DoAfter - │ -3. DoAfter успешно стартовал → RaiseLocalEvent(tool, UseToolEvent) - │ -4. WeldingSparksSystem.OnUseTool() на СЕРВЕРЕ: - ├── PlayToolSound() — звук сварки (-2дБ) - ├── GetSpawnLoc() → координаты шлюза - ├── Spawn("EchoEffectWeldingSparks") → создаёт эффект - ├── SpawnedEffects[doAfterId] = effect → запоминает - └── RaiseNetworkEvent(SpawnedWeldingSparksEvent) → клиентам - │ -5. WeldingSparksAnimationSystem.OnSpawnedWeldingSparks() на КАЖДОМ КЛИЕНТЕ: - ├── Проверяет WeldableComponent + WeldingSparksAnimationComponent - ├── GetOffsets() → (0,0.5) → (0,-0.5) с учётом поворота - ├── Создаёт Animation с линейной интерполяцией Offset - └── _animation.Play() → эффект начинает двигаться - │ -6. Игрок видит: искры + дым плавно скользят по шлюзу - сверху вниз за время сварки - │ -7. DoAfter завершается (успех или отмена) - │ -8. WeldingSparksSystem.OnAfterUseTool() на СЕРВЕРЕ: - ├── QueueDel(effect) → удаляет эффект - └── SpawnedEffects.Remove() → чистит словарь -``` - ---- - -## Частые вопросы для новых кодеров - -### Зачем два отдельных компонента (`WeldingSparksComponent` и `WeldingSparksAnimationComponent`)? - -Разделение ответственности: -- `WeldingSparksComponent` на **инструменте** — управляет СПАВНОМ эффекта (что именно спавнить, отслеживание активных эффектов). -- `WeldingSparksAnimationComponent` на **цели** — управляет АНИМАЦИЕЙ (как двигать спрайт). - -Если бы всё было в одном компоненте, пришлось бы добавлять его и к сваркам, и ко всем дверям, и логика мешалась бы. - -### Почему звук сварки проигрывается в `OnUseTool()`, а не в дефолтном `OnDoAfter()`? - -В стандартной логике SS14 звук инструмента проигрывается при **завершении** DoAfter (в `OnDoAfter`). Но для сварки хотелось, чтобы звук начинался **сразу** при начале использования. Поэтому `WeldingSparksSystem.OnUseTool()` вызывает `PlayToolSound()` самостоятельно. Звук немного тише (`-2дБ`), чтобы не перекрывать другие звуки. - -### Как работает анимация при повороте двери? - -`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`. - -### Можно ли добавить искры к новому свариваемому объекту? - -Да. Нужно: -1. Убедиться, что объект имеет `WeldableComponent` (стандартный SS14). -2. Добавить `WeldingSparksAnimationComponent` в его YAML-прототип: -```yaml - - type: WeldingSparksAnimation - startingOffset: X, Y # Откуда начинают двигаться искры - endingOffset: X, Y # (необязательно) Куда заканчивают -``` - -### Почему сетевое событие отправляется всем, а не конкретным игрокам? - -В отличие от барков (где сервер вручную ищет игроков в радиусе и шлёт каждому), здесь используется `RaiseNetworkEvent()` без фильтра — событие летит всем. Это допустимо потому что: -1. Сущность-эффект уже спавнена на позиции цели — клиент и так видит её. -2. Анимация — чисто косметическая, без геймплей-последствий. -3. Клиент сам проверяет наличие нужных компонентов и отказывается от анимации если их нет.