diff --git a/Content.Shared/ADT/Vehicle/Trailer/ADTTrailerComponent.cs b/Content.Shared/ADT/Vehicle/Trailer/ADTTrailerComponent.cs
new file mode 100644
index 00000000000..dbd161b1fc2
--- /dev/null
+++ b/Content.Shared/ADT/Vehicle/Trailer/ADTTrailerComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.ADT.Vehicle.Trailer;
+
+///
+/// Маркер прицепа: каталка или мешок для трупов, цепляемый к сцепке транспорта.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ADTTrailerComponent : Component
+{
+}
\ No newline at end of file
diff --git a/Content.Shared/ADT/Vehicle/Trailer/ADTVehicleHitchComponent.cs b/Content.Shared/ADT/Vehicle/Trailer/ADTVehicleHitchComponent.cs
new file mode 100644
index 00000000000..7f98e70281d
--- /dev/null
+++ b/Content.Shared/ADT/Vehicle/Trailer/ADTVehicleHitchComponent.cs
@@ -0,0 +1,46 @@
+using System.Numerics;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.ADT.Vehicle.Trailer;
+
+///
+/// Компонент транспорта со сцепкой: создаёт дочернюю сущность-сцепку, к которой цепляются прицепы.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ADTVehicleHitchComponent : Component
+{
+ ///
+ /// Прототип создаваемой сцепки.
+ ///
+ [DataField]
+ public EntProtoId HitchPrototype = "ADTVehicleHitch";
+
+ ///
+ /// Смещение сцепки относительно транспорта.
+ ///
+ [DataField]
+ public Vector2 HitchOffset = new(0, 0.55f);
+
+ ///
+ /// Радиус поиска прицепа вокруг сцепки.
+ ///
+ [DataField]
+ public float AttachRange = 2.5f;
+
+ ///
+ /// Действие водителя: прицепить или отцепить прицеп.
+ ///
+ [DataField]
+ public EntProtoId ToggleAction = "ADTActionTrailerToggle";
+
+ ///
+ /// Созданная сцепка.
+ ///
+ public EntityUid? Hitch;
+
+ ///
+ /// Созданное действие прицепа.
+ ///
+ public EntityUid? ToggleActionEntity;
+}
\ No newline at end of file
diff --git a/Content.Shared/ADT/Vehicle/Trailer/ADTVehicleHitchStrapComponent.cs b/Content.Shared/ADT/Vehicle/Trailer/ADTVehicleHitchStrapComponent.cs
new file mode 100644
index 00000000000..f4bd5f9b826
--- /dev/null
+++ b/Content.Shared/ADT/Vehicle/Trailer/ADTVehicleHitchStrapComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.ADT.Vehicle.Trailer;
+
+///
+/// Маркер сущности-сцепки транспорта.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class ADTVehicleHitchStrapComponent : Component
+{
+}
\ No newline at end of file
diff --git a/Content.Shared/ADT/Vehicle/Trailer/SharedADTVehicleTrailerSystem.cs b/Content.Shared/ADT/Vehicle/Trailer/SharedADTVehicleTrailerSystem.cs
new file mode 100644
index 00000000000..00e78910c52
--- /dev/null
+++ b/Content.Shared/ADT/Vehicle/Trailer/SharedADTVehicleTrailerSystem.cs
@@ -0,0 +1,242 @@
+using System.Linq;
+using System.Numerics;
+using Content.Shared.Actions;
+using Content.Shared.Actions.Components;
+using Content.Shared.Buckle;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Containers;
+using Content.Shared.Foldable;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Vehicle.Components;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+
+namespace Content.Shared.ADT.Vehicle.Trailer;
+
+///
+/// Событие действия водителя: прицепить или отцепить прицеп от сцепки.
+///
+public sealed partial class ADTTrailerToggleActionEvent : InstantActionEvent
+{
+}
+
+///
+/// Сцепки транспорта и прицепы: создание сцепки, прицепление и отцепление каталоги/мешков для трупов.
+///
+public sealed partial class SharedADTVehicleTrailerSystem : EntitySystem
+{
+ [Dependency] private readonly INetManager _netManager = default!;
+ [Dependency] private readonly SharedBuckleSystem _buckle = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnHitchMapInit);
+ SubscribeLocalEvent(OnVehicleTerminating);
+ SubscribeLocalEvent(OnVehicleStrapped);
+ SubscribeLocalEvent(OnVehicleUnstrapped);
+ SubscribeLocalEvent(OnTrailerToggleAction);
+
+ SubscribeLocalEvent(OnTrailerInteractHand);
+ SubscribeLocalEvent(OnTrailerFolded);
+ SubscribeLocalEvent(OnTrailerTerminating);
+ }
+
+ private void OnHitchMapInit(Entity ent, ref MapInitEvent args)
+ {
+ if (!_netManager.IsServer || ent.Comp.Hitch != null)
+ return;
+
+ var hitch = Spawn(ent.Comp.HitchPrototype, Transform(ent).Coordinates);
+ if (!TryComp(hitch, out _))
+ {
+ Log.Error($"Failed to spawn hitch {ent.Comp.HitchPrototype} for {ToPrettyString(ent)}");
+ return;
+ }
+
+ ent.Comp.Hitch = hitch;
+ Dirty(ent);
+
+ _transform.SetCoordinates(hitch, new EntityCoordinates(ent.Owner, ent.Comp.HitchOffset));
+ }
+
+ private void OnVehicleTerminating(Entity ent, ref EntityTerminatingEvent args)
+ {
+ if (ent.Comp.Hitch is not { } hitch)
+ return;
+
+ // Выкинуть прицепы на карту, иначе они удалятся вместе с транспортом:
+ // при Terminating транспорта ванильный Unbuckle пропускает PlaceNextTo
+ if (TryComp(hitch, out var strap))
+ {
+ foreach (var buckled in strap.BuckledEntities.ToArray())
+ {
+ var xform = Transform(buckled);
+ _transform.SetCoordinates(buckled, xform, _transform.ToCoordinates(_transform.ToMapCoordinates(xform.Coordinates)));
+ _buckle.Unbuckle(buckled, null);
+ }
+ }
+ // Хич - ребёнок транспорта, движок удалит его сам в RecursiveFlagEntityTermination
+ }
+
+ private void OnVehicleStrapped(Entity ent, ref StrappedEvent args)
+ {
+ var rider = args.Buckle.Owner;
+ if (!TryComp(rider, out var actions))
+ return;
+
+ _actions.AddAction(rider, ref ent.Comp.ToggleActionEntity, ent.Comp.ToggleAction, ent.Owner, actions);
+ }
+
+ private void OnVehicleUnstrapped(Entity ent, ref UnstrappedEvent args)
+ {
+ _actions.RemoveProvidedActions(args.Buckle.Owner, ent.Owner);
+ }
+
+ private void OnTrailerToggleAction(Entity ent, ref ADTTrailerToggleActionEvent args)
+ {
+ if (_netManager.IsClient)
+ return;
+
+ args.Handled = ToggleTrailer(ent, args.Performer);
+ }
+
+ private bool ToggleTrailer(Entity ent, EntityUid user)
+ {
+ // Действие только для текущего водителя этого транспорта
+ if (!TryComp(user, out var rider) || rider.Vehicle != ent.Owner)
+ return false;
+
+ if (ent.Comp.Hitch is not { } hitch || !TryComp(hitch, out var strap))
+ return false;
+
+ if (strap.BuckledEntities.Count == 0)
+ {
+ if (!TryFindTrailer(ent, hitch, out var trailer) ||
+ !TryComp(trailer, out var buckle) ||
+ !_buckle.TryBuckle(trailer, user, hitch, buckle))
+ {
+ return false;
+ }
+
+ _popup.PopupEntity(Loc.GetString("adt-trailer-attached"), ent.Owner, user);
+ return true;
+ }
+
+ var buckled = strap.BuckledEntities.First();
+ // Прямой Unbuckle: CanUnbuckle блокируется коллизией квадроцикла между водителем и сцепкой
+ _buckle.Unbuckle(buckled, null);
+
+ // Отодвинуть прицеп от сцепки за корму, чтобы отцепление было заметно
+ var away = _transform.GetWorldPosition(hitch) - _transform.GetWorldPosition(ent);
+ if (away == Vector2.Zero)
+ away = new Vector2(0, 1);
+ away = Vector2.Normalize(away);
+
+ _transform.SetWorldPosition(buckled, _transform.GetWorldPosition(hitch) + away * 0.8f);
+
+ _popup.PopupEntity(Loc.GetString("adt-trailer-unattached"), ent.Owner, user);
+ return true;
+ }
+
+ private void OnTrailerInteractHand(Entity ent, ref InteractHandEvent args)
+ {
+ if (args.Handled || !_netManager.IsServer)
+ return;
+
+ if (!TryComp(ent, out var buckle) || buckle.BuckledTo != null)
+ return;
+
+ if (!TryFindHitch(ent.Owner, out var hitch))
+ return;
+
+ if (!_buckle.TryBuckle(ent.Owner, args.User, hitch, buckle))
+ return;
+
+ args.Handled = true;
+ _popup.PopupEntity(Loc.GetString("adt-trailer-attached"), ent.Owner, args.User);
+ }
+
+ private void OnTrailerFolded(Entity ent, ref FoldedEvent args)
+ {
+ if (!args.IsFolded || !_netManager.IsServer)
+ return;
+
+ if (!TryComp(ent, out var buckle) || buckle.BuckledTo == null)
+ return;
+
+ _buckle.Unbuckle(ent.Owner, null);
+ }
+
+ private void OnTrailerTerminating(Entity ent, ref EntityTerminatingEvent args)
+ {
+ // Убрать прицеп из списка сцепки, чтобы не осталось мёртвого uid
+ if (TryComp(ent, out var buckle) && buckle.BuckledTo != null)
+ _buckle.Unbuckle(ent.Owner, null);
+ }
+
+ private bool TryFindTrailer(Entity ent, EntityUid hitch, out EntityUid trailer)
+ {
+ trailer = default;
+ var hitchPos = _transform.ToMapCoordinates(Transform(hitch).Coordinates);
+ var maxRangeSq = ent.Comp.AttachRange * ent.Comp.AttachRange;
+
+ var bestDist = maxRangeSq;
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var candidate, out _, out var buckle, out var xform))
+ {
+ if (buckle.BuckledTo != null || _container.IsEntityInContainer(candidate))
+ continue;
+
+ if (xform.MapID != hitchPos.MapId)
+ continue;
+
+ var dist = (hitchPos.Position - _transform.ToMapCoordinates(xform.Coordinates).Position).LengthSquared();
+ if (dist > bestDist)
+ continue;
+
+ bestDist = dist;
+ trailer = candidate;
+ }
+
+ return trailer != default;
+ }
+
+ private bool TryFindHitch(EntityUid trailer, out EntityUid hitch)
+ {
+ hitch = default;
+ var trailerPos = _transform.ToMapCoordinates(Transform(trailer).Coordinates);
+
+ var bestDist = float.MaxValue;
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp, out var xform))
+ {
+ if (comp.Hitch is not { } hitchUid)
+ continue;
+
+ if (xform.MapID != trailerPos.MapId)
+ continue;
+
+ // Только свободная сцепка: один прицеп на одну сцепку
+ if (!TryComp(hitchUid, out var strap) || strap.BuckledEntities.Count != 0)
+ continue;
+
+ var range = comp.AttachRange;
+ var dist = (trailerPos.Position - _transform.ToMapCoordinates(xform.Coordinates).Position).LengthSquared();
+ if (dist > range * range || dist > bestDist)
+ continue;
+
+ bestDist = dist;
+ hitch = hitchUid;
+ }
+
+ return hitch != default;
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
index 3538ce9ca56..580e193c179 100644
--- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
+++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
+using Content.Shared.ADT.Vehicle.Trailer;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.Cuffs.Components;
@@ -141,6 +142,10 @@ private void BuckleTransformCheck(Entity buckle, TransformCompo
if (HasComp(buckle.Owner) && HasComp(strapUid))
return;
// ADT Vehicles end
+ // ADT-Tweak-Start: прицепы (каталка, мешок для трупов) не должны отцепляться от сцепки транспорта
+ if (HasComp(buckle.Owner) && HasComp(strapUid))
+ return;
+ // ADT-Tweak-End
var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared();
if (delta > 1e-5)
diff --git a/Resources/Locale/en-US/ADT/prototypes/Entities/Objects/Vehicles/vehicles.ftl b/Resources/Locale/en-US/ADT/prototypes/Entities/Objects/Vehicles/vehicles.ftl
new file mode 100644
index 00000000000..7a659a0c33e
--- /dev/null
+++ b/Resources/Locale/en-US/ADT/prototypes/Entities/Objects/Vehicles/vehicles.ftl
@@ -0,0 +1,8 @@
+ent-ADTVehicleATVMedic = medical ATV
+ .desc = An ATV with a hitch for carrying rollerbeds and body bags.
+
+ent-ADTActionTrailerToggle = Trailer
+ .desc = Attach or detach a trailer from the ATV hitch.
+
+adt-trailer-attached = The trailer is attached to the ATV.
+adt-trailer-unattached = The trailer is detached from the ATV.
diff --git a/Resources/Locale/ru-RU/ADT/prototypes/Entities/Objects/Vehicles/vehicles.ftl b/Resources/Locale/ru-RU/ADT/prototypes/Entities/Objects/Vehicles/vehicles.ftl
index 7aa69aec83c..49f63c2a62c 100644
--- a/Resources/Locale/ru-RU/ADT/prototypes/Entities/Objects/Vehicles/vehicles.ftl
+++ b/Resources/Locale/ru-RU/ADT/prototypes/Entities/Objects/Vehicles/vehicles.ftl
@@ -40,3 +40,12 @@ vehicle-use-key = Вы используете { $keys } чтобы запуст
vehicle-folded-cannot-buckle = Нельзя сесть на сложенное транспортное средство { $vehicle }.
vehicle-hands-occupied = Руки заняты, чтобы сесть на { $vehicle }.
vehicle-folded-ejected = Вас вытолкнули из { $vehicle }, потому что его сложили!
+
+ent-ADTVehicleATVMedic = медицинский квадроцикл
+ .desc = Квадроцикл со сцепкой для перевозки каталок и мешков для трупов.
+
+ent-ADTActionTrailerToggle = прицеп
+ .desc = Прицепить или отцепить прицеп от сцепки квадроцикла.
+
+adt-trailer-attached = Прицеп закреплён на сцепке.
+adt-trailer-unattached = Прицеп отцеплён от сцепки.
diff --git a/Resources/Prototypes/ADT/Entities/Markers/Spawners/vehicles.yml b/Resources/Prototypes/ADT/Entities/Markers/Spawners/vehicles.yml
index 6e1e9c33032..320c79575aa 100644
--- a/Resources/Prototypes/ADT/Entities/Markers/Spawners/vehicles.yml
+++ b/Resources/Prototypes/ADT/Entities/Markers/Spawners/vehicles.yml
@@ -95,3 +95,12 @@
- type: ConditionalSpawner
prototypes:
- ADTVehicleLavabike
+
+- type: entity
+ name: Medical ATV Spawner
+ id: ADTSpawnVehicleATVMedic
+ parent: ADTSpawnVehicleATV
+ components:
+ - type: ConditionalSpawner
+ prototypes:
+ - ADTVehicleATVMedic
diff --git a/Resources/Prototypes/ADT/Entities/Objects/Vehicles/actions.yml b/Resources/Prototypes/ADT/Entities/Objects/Vehicles/actions.yml
index db37fec90a1..4800871e17d 100644
--- a/Resources/Prototypes/ADT/Entities/Objects/Vehicles/actions.yml
+++ b/Resources/Prototypes/ADT/Entities/Objects/Vehicles/actions.yml
@@ -9,3 +9,12 @@
icon: { sprite: Objects/Fun/bikehorn.rsi, state: icon }
- type: InstantAction
event: !type:HonkActionEvent
+
+- type: entity
+ id: ADTActionTrailerToggle
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: Action
+ icon: { sprite: Structures/Furniture/rollerbeds.rsi, state: rollerbed }
+ - type: InstantAction
+ event: !type:ADTTrailerToggleActionEvent
diff --git a/Resources/Prototypes/ADT/Entities/Objects/Vehicles/buckleable.yml b/Resources/Prototypes/ADT/Entities/Objects/Vehicles/buckleable.yml
index 3ce33ce0755..e3fd6ce5aed 100644
--- a/Resources/Prototypes/ADT/Entities/Objects/Vehicles/buckleable.yml
+++ b/Resources/Prototypes/ADT/Entities/Objects/Vehicles/buckleable.yml
@@ -282,6 +282,7 @@
components:
- Storage
sprite: Objects/Vehicles/atv.rsi
+ - type: ADTVehicleHitch
- type: entity
id: ADTVehicleSyndicateSegway
@@ -480,4 +481,8 @@
suffix: folded
components:
- type: Foldable
- folded: true
\ No newline at end of file
+ folded: true
+
+- type: entity
+ parent: ADTVehicleATV
+ id: ADTVehicleATVMedic
diff --git a/Resources/Prototypes/ADT/Entities/Objects/Vehicles/trailer.yml b/Resources/Prototypes/ADT/Entities/Objects/Vehicles/trailer.yml
new file mode 100644
index 00000000000..df2080a95ee
--- /dev/null
+++ b/Resources/Prototypes/ADT/Entities/Objects/Vehicles/trailer.yml
@@ -0,0 +1,15 @@
+- type: entity
+ id: ADTVehicleHitch
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: ADTVehicleHitchStrap
+ - type: Physics
+ bodyType: KinematicController
+ bodyStatus: OnGround
+ canCollide: false
+ - type: Strap
+ buckleOnInteractHand: false
+ position: None
+ whitelist:
+ components:
+ - ADTTrailer
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml
index c899ebe86af..d7462d6d951 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml
@@ -87,6 +87,11 @@
True: {visible: false}
False: {visible: true}
- type: Pullable
+ # ADT-Tweak-Start: мешок для трупов цепляется к сцепке квадроцикла
+ - type: Buckle
+ range: 3
+ - type: ADTTrailer
+ # ADT-Tweak-End
- type: AntiRottingContainer
- type: ContainerContainer
containers:
diff --git a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml
index 4c5fc44dd8d..1d6f38a1c73 100644
--- a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml
+++ b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml
@@ -64,6 +64,11 @@
rotation: -90
buckleOffset: "0,0.15"
buckleOnInteractHand: False
+ # ADT-Tweak-Start: каталка цепляется к сцепке квадроцикла
+ - type: Buckle
+ range: 3
+ - type: ADTTrailer
+ # ADT-Tweak-End
- type: Appearance
- type: GenericVisualizer
visuals: