diff --git a/Content.Pirate.Client/_JustDecor/Weapons/SmartRevolver/SmartRevolverOverlay.cs b/Content.Pirate.Client/_JustDecor/Weapons/SmartRevolver/SmartRevolverOverlay.cs
new file mode 100644
index 000000000000..040a00829491
--- /dev/null
+++ b/Content.Pirate.Client/_JustDecor/Weapons/SmartRevolver/SmartRevolverOverlay.cs
@@ -0,0 +1,97 @@
+using System.Numerics;
+using Content.Pirate.Shared._JustDecor.Weapons.SmartRevolver;
+using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Shared.Enums;
+using Robust.Shared.Maths;
+
+namespace Content.Pirate.Client._JustDecor.Weapons.SmartRevolver;
+
+///
+/// Overlay that shows a target indicator around the selected entity for the smart revolver.
+///
+public sealed class SmartRevolverOverlay : Overlay
+{
+ private readonly IEntityManager _entityManager;
+ private readonly IPlayerManager _playerManager;
+ private readonly IEyeManager _eyeManager;
+ private readonly SharedHandsSystem _hands;
+
+ public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+
+ public SmartRevolverOverlay(IEntityManager entityManager, IPlayerManager playerManager, IEyeManager eyeManager)
+ {
+ _entityManager = entityManager;
+ _playerManager = playerManager;
+ _eyeManager = eyeManager;
+ _hands = _entityManager.System();
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var player = _playerManager.LocalSession?.AttachedEntity;
+ if (player == null)
+ return;
+
+ if (!_hands.TryGetActiveItem(player.Value, out var activeHandEntity))
+ return;
+
+ if (!_entityManager.TryGetComponent(activeHandEntity, out var comp))
+ return;
+
+ if (comp.SelectedTarget == null || !_entityManager.EntityExists(comp.SelectedTarget.Value))
+ return;
+
+ if (!_entityManager.TryGetComponent(comp.SelectedTarget.Value, out var targetXform))
+ return;
+
+ if (targetXform.MapID != args.MapId)
+ return;
+
+ var screenPos = _eyeManager.CoordinatesToScreen(targetXform.Coordinates);
+ var handle = args.ScreenHandle;
+ var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f;
+
+ // Прицільний індикатор
+ var color = Color.Gold;
+ var boxSize = new Vector2(60f, 60f) * uiScale;
+ var halfSize = boxSize / 2;
+ var topLeft = screenPos.Position - halfSize;
+ var borderThickness = 2.5f * uiScale;
+
+ // Зовнішня рамка
+ handle.DrawRect(UIBox2.FromDimensions(topLeft, new Vector2(boxSize.X, borderThickness)), color);
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(0, boxSize.Y - borderThickness), new Vector2(boxSize.X, borderThickness)), color);
+ handle.DrawRect(UIBox2.FromDimensions(topLeft, new Vector2(borderThickness, boxSize.Y)), color);
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - borderThickness, 0), new Vector2(borderThickness, boxSize.Y)), color);
+
+ // Кути для "lock-on" ефекту
+ var cornerLength = 12f * uiScale;
+ var cornerThickness = 3f * uiScale;
+ var cornerOffset = 5f * uiScale;
+
+ // Верхній лівий кут
+ handle.DrawRect(UIBox2.FromDimensions(topLeft - new Vector2(cornerOffset, cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
+ handle.DrawRect(UIBox2.FromDimensions(topLeft - new Vector2(cornerOffset, cornerOffset), new Vector2(cornerThickness, cornerLength)), color);
+
+ // Верхній правий кут
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerLength + cornerOffset, -cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerThickness + cornerOffset, -cornerOffset), new Vector2(cornerThickness, cornerLength)), color);
+
+ // Нижній лівий кут
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(-cornerOffset, boxSize.Y - cornerThickness + cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(-cornerOffset, boxSize.Y - cornerLength + cornerOffset), new Vector2(cornerThickness, cornerLength)), color);
+
+ // Нижній правий кут
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerLength + cornerOffset, boxSize.Y - cornerThickness + cornerOffset), new Vector2(cornerLength, cornerThickness)), color);
+ handle.DrawRect(UIBox2.FromDimensions(topLeft + new Vector2(boxSize.X - cornerThickness + cornerOffset, boxSize.Y - cornerLength + cornerOffset), new Vector2(cornerThickness, cornerLength)), color);
+
+ // Центральна частинка
+ var diamondSize = 5f * uiScale;
+ handle.DrawRect(UIBox2.FromDimensions(screenPos.Position - new Vector2(diamondSize / 2, diamondSize / 2), new Vector2(diamondSize, diamondSize)), color);
+ }
+}
diff --git a/Content.Pirate.Client/_JustDecor/Weapons/SmartRevolver/SmartRevolverSystem.cs b/Content.Pirate.Client/_JustDecor/Weapons/SmartRevolver/SmartRevolverSystem.cs
new file mode 100644
index 000000000000..4858af70d8b9
--- /dev/null
+++ b/Content.Pirate.Client/_JustDecor/Weapons/SmartRevolver/SmartRevolverSystem.cs
@@ -0,0 +1,113 @@
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Client.Input;
+using Robust.Shared.Input;
+using Robust.Shared.Input.Binding;
+using Content.Shared.CombatMode;
+using Robust.Shared.Network;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.IoC;
+using Content.Pirate.Shared._JustDecor.Weapons.SmartRevolver;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Damage;
+using Robust.Shared.Player;
+using Content.Shared.Hands.EntitySystems;
+using Content.Client.ContextMenu.UI;
+using Content.Client.UserInterface.Systems.Actions;
+using Content.Client.UserInterface.Systems.Hands;
+using Content.Client.UserInterface.Systems.Inventory;
+using Content.Client.UserInterface.Systems.Storage;
+
+namespace Content.Pirate.Client._JustDecor.Weapons.SmartRevolver;
+
+public sealed class SmartRevolverSystem : EntitySystem
+{
+ [Dependency] private readonly SharedCombatModeSystem _combat = default!;
+ [Dependency] private readonly IOverlayManager _overlay = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IEyeManager _eye = default!;
+ [Dependency] private readonly IEntityManager _entity = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ private SmartRevolverOverlay _overlayInst = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _overlayInst = new SmartRevolverOverlay(_entity, _player, _eye);
+ _overlay.AddOverlay(_overlayInst);
+ CommandBinds.Builder
+ .BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnRightClick),
+ typeof(EntityMenuUIController),
+ typeof(HandsUIController),
+ typeof(InventoryUIController),
+ typeof(StorageUIController),
+ typeof(ActionUIController))
+ .Register();
+ }
+
+ private bool OnRightClick(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ return TryHandleInstantTargeting(session, coords, uid);
+ }
+
+ private bool TryHandleInstantTargeting(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
+ {
+ if (session == null)
+ return false;
+
+ var player = session?.AttachedEntity;
+ if (player == null)
+ return false;
+
+ // Instant targeting only in combat mode
+ if (!_combat.IsInCombatMode(player.Value))
+ return false;
+
+ // Must be holding Smart Revolver
+ if (!_hands.TryGetActiveItem(player.Value, out var activeItem))
+ return false;
+
+ if (!TryComp(activeItem.Value, out SmartRevolverComponent? revolver))
+ return false;
+
+ EntityUid target = uid;
+
+ if (!target.IsValid() || target == player || target == activeItem.Value)
+ {
+ RaiseNetworkEvent(new SmartRevolverSetTargetMessage(NetEntity.Invalid));
+ return true;
+ }
+
+ var playerPos = _transform.GetMapCoordinates(player.Value).Position;
+ var targetPos = target.IsValid()
+ ? _transform.GetMapCoordinates(target).Position
+ : _transform.ToMapCoordinates(coords).Position;
+
+ if ((targetPos - playerPos).Length() > revolver.MaxTargetDistance)
+ {
+ RaiseNetworkEvent(new SmartRevolverSetTargetMessage(NetEntity.Invalid));
+ return true;
+ }
+
+ if (HasComp(target) || HasComp(target))
+ {
+ RaiseNetworkEvent(new SmartRevolverSetTargetMessage(GetNetEntity(target)));
+ return true;
+ }
+
+ // Клік в пусте місце -> очищення цілі
+ RaiseNetworkEvent(new SmartRevolverSetTargetMessage(NetEntity.Invalid));
+ return true;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _overlay.RemoveOverlay(_overlayInst);
+ CommandBinds.Unregister();
+ }
+}
diff --git a/Content.Pirate.Shared/_JustDecor/Weapons/Ranged/RicochetProjectileComponent.cs b/Content.Pirate.Shared/_JustDecor/Weapons/Ranged/RicochetProjectileComponent.cs
new file mode 100644
index 000000000000..ac461e929457
--- /dev/null
+++ b/Content.Pirate.Shared/_JustDecor/Weapons/Ranged/RicochetProjectileComponent.cs
@@ -0,0 +1,91 @@
+using System.Numerics;
+using Robust.Shared.GameStates;
+
+namespace Content.Pirate.Shared._JustDecor.Weapons.Ranged;
+
+///
+/// Component for projectiles that can ricochet off walls to hit a target.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class RicochetProjectileComponent : Component
+{
+ ///
+ /// The target entity that the projectile should try to hit via ricochets.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityUid? Target;
+
+ ///
+ /// Maximum number of bounces allowed before the projectile stops ricocheting.
+ ///
+ [DataField, AutoNetworkedField]
+ public int MaxBounces = 4;
+
+ ///
+ /// Current number of bounces that have occurred.
+ ///
+ [DataField, AutoNetworkedField]
+ public int CurrentBounces = 0;
+
+ ///
+ /// Planned waypoints for the ricochet path (wall hit positions).
+ ///
+ [DataField]
+ public List PlannedPath = new();
+
+ ///
+ /// Whether the projectile should follow the planned path.
+ /// If false, the projectile will calculate ricochets dynamically on each bounce.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool FollowPlannedPath = true;
+
+ ///
+ /// Minimum speed required for ricochet to occur. Below this speed, projectile will stop.
+ ///
+ [DataField]
+ public float MinimumRicochetSpeed = 5f;
+
+ ///
+ /// Speed multiplier applied after each bounce (energy loss).
+ ///
+ [DataField]
+ public float SpeedRetentionOnBounce = 0.95f;
+
+ ///
+ /// How strongly the projectile steers towards its target every frame.
+ /// 0.0 means no steering, 1.0 means instant snap.
+ ///
+ [DataField]
+ public float SteeringStrength = 1.0f; // High default for 100% hits
+
+ ///
+ /// Delay before homing starts (seconds).
+ ///
+ [DataField]
+ public float HomingDelay = 0.2f;
+
+ ///
+ /// Current accumulator for homing delay.
+ ///
+ [ViewVariables]
+ public float HomingAccumulator = 0f;
+
+ ///
+ /// How much speed to add per bounce.
+ ///
+ [DataField]
+ public float SpeedBonusPerBounce = 5f;
+
+ ///
+ /// Lifetime bonus applied after each bounce.
+ ///
+ [DataField]
+ public float BounceLifetimeBonus = 3f;
+
+ ///
+ /// Target number of bounces before hitting the final target.
+ ///
+ [DataField]
+ public int TargetBounces = 0;
+}
diff --git a/Content.Pirate.Shared/_JustDecor/Weapons/Ranged/RicochetProjectileSystem.cs b/Content.Pirate.Shared/_JustDecor/Weapons/Ranged/RicochetProjectileSystem.cs
new file mode 100644
index 000000000000..cb406e03b6a0
--- /dev/null
+++ b/Content.Pirate.Shared/_JustDecor/Weapons/Ranged/RicochetProjectileSystem.cs
@@ -0,0 +1,437 @@
+using System.Numerics;
+using Content.Shared.Physics;
+using Content.Shared.Projectiles;
+using Content.Shared.Damage;
+using Robust.Shared.Map;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Spawners;
+
+namespace Content.Pirate.Shared._JustDecor.Weapons.Ranged;
+
+///
+/// System that handles ricochet projectiles with improved tracking and collision handling.
+///
+public sealed class RicochetProjectileSystem : EntitySystem
+{
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+
+ private const float MaxSearchRadius = 50f;
+ private const float MinWallDistance = 0.45f;
+ private const float AngleSearchStep = 10f;
+ private const int MaxRaycasts = 256;
+ private const float SteeringResponsiveness = 50f;
+ private const float MinHomingDelay = 0.05f;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnProjectileHit);
+ SubscribeLocalEvent(OnPreventCollide);
+ }
+
+ private void OnStartup(EntityUid uid, RicochetProjectileComponent component, ComponentStartup args)
+ {
+ if (component.Target == null || !component.Target.Value.IsValid() || !TryComp(component.Target.Value, out TransformComponent? _))
+ {
+ component.FollowPlannedPath = false;
+ return;
+ }
+
+ component.HomingAccumulator = component.HomingDelay;
+ CalculateRicochetPath(uid, component);
+ }
+
+ private void OnPreventCollide(EntityUid uid, RicochetProjectileComponent component, ref PreventCollideEvent args)
+ {
+ if (component.Target == null || !component.Target.Value.IsValid())
+ return;
+
+ var maxBounces = component.MaxBounces <= 0 ? component.TargetBounces : Math.Min(component.TargetBounces, component.MaxBounces);
+
+ // Don't collide with the target UNTIL we have finished bounces or have LoS shortcut
+ if (args.OtherEntity == component.Target)
+ {
+ // If we have LoS and at least one bounce, we SHOULD collide
+ if (component.CurrentBounces >= 1)
+ {
+ if (!component.FollowPlannedPath || component.CurrentBounces >= maxBounces)
+ {
+ if (!TryComp(component.Target.Value, out TransformComponent? targetXform))
+ return;
+
+ var currentPos = _transform.GetWorldPosition(uid);
+ var targetPos = _transform.GetWorldPosition(targetXform);
+ if (HasDirectLineOfSight(currentPos, targetPos, Transform(uid).MapID, component.Target.Value))
+ {
+ return; // Allow collision
+ }
+ }
+ }
+
+ if (component.CurrentBounces < maxBounces && component.FollowPlannedPath)
+ {
+ args.Cancelled = true;
+ }
+ }
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component, out var physics, out var xform))
+ {
+ // Reduce homing delay
+ if (component.HomingAccumulator > 0)
+ component.HomingAccumulator -= frameTime;
+
+ // Sync rotation
+ var currentVelocity = physics.LinearVelocity;
+ var currentSpeed = currentVelocity.Length();
+
+ if (currentSpeed > 0.1f)
+ {
+ _transform.SetWorldRotation(uid, currentVelocity.ToWorldAngle());
+ }
+
+ if (component.Target == null || !component.Target.Value.IsValid() || Deleted(component.Target.Value))
+ continue;
+
+ if (!TryComp(component.Target.Value, out TransformComponent? targetXform))
+ continue;
+
+ var currentPos = _transform.GetWorldPosition(xform);
+ var targetPos = _transform.GetWorldPosition(targetXform);
+ var dist = (targetPos - currentPos).Length();
+
+ // Manual hit fallback to prevent orbiting/flying through hitboxes
+ if (dist < 0.6f)
+ {
+ HandleTargetHit(uid, component);
+ continue;
+ }
+
+ // Homing logic
+ var hasLoS = component.CurrentBounces >= 1 && HasDirectLineOfSight(currentPos, targetPos, xform.MapID, component.Target.Value);
+
+ if (component.HomingAccumulator <= 0 && (component.CurrentBounces >= component.TargetBounces || hasLoS))
+ {
+ var towardsTarget = (targetPos - currentPos).Normalized();
+ var currentDir = currentVelocity.Normalized();
+
+ if (currentSpeed < 1f) continue;
+
+ // Very aggressive steering
+ var responsiveness = component.SteeringStrength * SteeringResponsiveness;
+ var alpha = 1f - MathF.Exp(-responsiveness * frameTime);
+ var newDir = Vector2.Normalize(Vector2.Lerp(currentDir, towardsTarget, MathF.Min(alpha, 1.0f)));
+ _physics.SetLinearVelocity(uid, newDir * currentSpeed, body: physics);
+
+ // Keep projectile alive for ricochet handling.
+ }
+ }
+ }
+
+ private void HandleTargetHit(EntityUid uid, RicochetProjectileComponent component)
+ {
+ if (Deleted(uid) || component.Target == null)
+ return;
+
+ if (Deleted(component.Target.Value))
+ return;
+
+ if (TryComp(uid, out var proj))
+ {
+ var hitEvent = new ProjectileHitEvent(proj.Damage, component.Target.Value, proj.Shooter);
+ RaiseLocalEvent(uid, ref hitEvent);
+ _damageable.TryChangeDamage(component.Target.Value, hitEvent.Damage, proj.IgnoreResistances, origin: proj.Shooter);
+
+ proj.DeleteOnCollide = true;
+ proj.ProjectileSpent = true;
+ _physics.SetLinearVelocity(uid, Vector2.Zero);
+ PredictedQueueDel(uid);
+ }
+ }
+
+ private void OnProjectileHit(EntityUid uid, RicochetProjectileComponent component, ref ProjectileHitEvent args)
+ {
+ if (args.Target == component.Target)
+ {
+ PredictedQueueDel(uid);
+ return;
+ }
+
+ var maxBounces = component.MaxBounces <= 0 ? component.TargetBounces : Math.Min(component.TargetBounces, component.MaxBounces);
+ if (component.CurrentBounces >= maxBounces)
+ {
+ if (TryComp(uid, out var proj))
+ {
+ proj.DeleteOnCollide = true;
+ Dirty(uid, proj);
+ }
+ return;
+ }
+
+ if (HasComp(args.Target))
+ return;
+
+ if (!TryComp(uid, out var physics))
+ return;
+
+ // Apply speed and lifetime bonus
+ var speed = physics.LinearVelocity.Length();
+ speed *= component.SpeedRetentionOnBounce;
+ speed += component.SpeedBonusPerBounce;
+ if (speed < component.MinimumRicochetSpeed)
+ {
+ PredictedQueueDel(uid);
+ return;
+ }
+
+ if (TryComp(uid, out var timed))
+ timed.Lifetime += component.BounceLifetimeBonus;
+
+ component.CurrentBounces++;
+ component.HomingAccumulator = MathF.Max(component.HomingDelay, MinHomingDelay);
+
+ var currentPos = _transform.GetWorldPosition(uid);
+ var mapId = Transform(uid).MapID;
+
+ // Find normal to push out of wall
+ var normal = TryGetContactNormal(uid, args.Target, out var contactNormal)
+ ? contactNormal
+ : (currentPos - _transform.GetWorldPosition(args.Target)).Normalized();
+ _transform.SetWorldPosition(uid, currentPos + normal * 0.15f);
+
+ // Check for LoS shortcut
+ if (component.Target != null && TryComp(component.Target.Value, out TransformComponent? tXform))
+ {
+ var targetPos = _transform.GetWorldPosition(tXform);
+ if (HasDirectLineOfSight(currentPos, targetPos, mapId, component.Target.Value))
+ {
+ var direction = (targetPos - currentPos).Normalized();
+ _physics.SetLinearVelocity(uid, direction * speed, body: physics);
+ component.FollowPlannedPath = false;
+ ResetProjectileState(uid);
+ return;
+ }
+ }
+
+ // Follow path or dynamic bounce
+ if (component.FollowPlannedPath && component.PlannedPath.Count > component.CurrentBounces)
+ {
+ var nextWaypoint = component.PlannedPath[component.CurrentBounces];
+ var direction = (nextWaypoint - currentPos).Normalized();
+ _physics.SetLinearVelocity(uid, direction * speed, body: physics);
+ ResetProjectileState(uid);
+ }
+ else
+ {
+ var velocity = CalculateDynamicBounce(uid, physics, component.Target ?? EntityUid.Invalid, speed, normal);
+ _physics.SetLinearVelocity(uid, velocity, body: physics);
+ ResetProjectileState(uid);
+ }
+ }
+
+ private void ResetProjectileState(EntityUid uid)
+ {
+ if (TryComp(uid, out var proj))
+ {
+ proj.DeleteOnCollide = false;
+ proj.ProjectileSpent = false;
+ Dirty(uid, proj);
+ }
+ }
+
+ private void CalculateRicochetPath(EntityUid projectile, RicochetProjectileComponent component)
+ {
+ if (component.Target == null || !component.Target.Value.IsValid())
+ {
+ component.FollowPlannedPath = false;
+ return;
+ }
+
+ if (!TryComp(projectile, out TransformComponent? xform) || !TryComp(component.Target.Value, out TransformComponent? targetXform))
+ {
+ component.FollowPlannedPath = false;
+ return;
+ }
+
+ var startPos = _transform.GetWorldPosition(xform);
+ var targetPos = _transform.GetWorldPosition(targetXform);
+ var mapId = xform.MapID;
+
+ component.PlannedPath.Clear();
+
+ var visited = new HashSet();
+ var depth = component.MaxBounces <= 0 ? component.TargetBounces : Math.Min(component.TargetBounces, component.MaxBounces);
+ var raycastBudget = MaxRaycasts;
+ var path = TryRecursiveSolve(startPos, targetPos, mapId, depth, component.Target.Value, visited, 0, ref raycastBudget);
+
+ if (path != null)
+ {
+ component.PlannedPath.AddRange(path);
+ component.FollowPlannedPath = true;
+ if (path.Count > 0 && TryComp(projectile, out var physics))
+ {
+ var dir = (path[0] - startPos).Normalized();
+ _physics.SetLinearVelocity(projectile, dir * physics.LinearVelocity.Length(), body: physics);
+ }
+ }
+ else
+ {
+ component.FollowPlannedPath = false;
+ }
+ }
+
+ private List? TryRecursiveSolve(Vector2 current, Vector2 target, MapId mapId, int depth, EntityUid targetEnt, HashSet visited, int currentDepth, ref int raycastBudget)
+ {
+ if (depth == 0)
+ return HasDirectLineOfSight(current, target, mapId, targetEnt, ref raycastBudget) ? new List() : null;
+
+ if (currentDepth > 8 || raycastBudget <= 0) return null;
+
+ for (float angle = 0; angle < 360; angle += AngleSearchStep)
+ {
+ var dir = Angle.FromDegrees(angle).ToWorldVec();
+ var ray = new CollisionRay(current, dir, (int) (CollisionGroup.Impassable | CollisionGroup.MidImpassable | CollisionGroup.BulletImpassable));
+ if (!TryGetFirstRayHit(mapId, ray, MaxSearchRadius, out var hit, ref raycastBudget))
+ continue;
+
+ if (visited.Contains(hit.HitEntity)) continue;
+
+ var normal = (current - hit.HitPos).Normalized();
+ var nextPos = hit.HitPos + normal * MinWallDistance;
+
+ visited.Add(hit.HitEntity);
+ var subPath = TryRecursiveSolve(nextPos, target, mapId, depth - 1, targetEnt, visited, currentDepth + 1, ref raycastBudget);
+ visited.Remove(hit.HitEntity);
+
+ if (subPath != null)
+ {
+ subPath.Insert(0, hit.HitPos);
+ return subPath;
+ }
+ }
+ return null;
+ }
+
+ private bool HasDirectLineOfSight(Vector2 from, Vector2 to, MapId mapId, EntityUid targetEntity)
+ {
+ var direction = (to - from).Normalized();
+ var distance = (to - from).Length();
+ if (distance < 0.2f) return true;
+
+ var ray = new CollisionRay(from, direction, (int) (CollisionGroup.Impassable | CollisionGroup.MidImpassable | CollisionGroup.BulletImpassable));
+ if (!TryGetFirstRayHit(mapId, ray, distance, out var hit))
+ return true;
+
+ return hit.HitEntity == targetEntity;
+ }
+
+ private bool HasDirectLineOfSight(Vector2 from, Vector2 to, MapId mapId, EntityUid targetEntity, ref int raycastBudget)
+ {
+ var direction = (to - from).Normalized();
+ var distance = (to - from).Length();
+ if (distance < 0.2f) return true;
+
+ if (raycastBudget <= 0)
+ return false;
+
+ var ray = new CollisionRay(from, direction, (int) (CollisionGroup.Impassable | CollisionGroup.MidImpassable | CollisionGroup.BulletImpassable));
+ if (!TryGetFirstRayHit(mapId, ray, distance, out var hit, ref raycastBudget))
+ return true;
+
+ return hit.HitEntity == targetEntity;
+ }
+
+ private bool TryGetFirstRayHit(MapId mapId, CollisionRay ray, float distance, out RayCastResults hit)
+ {
+ foreach (var result in _physics.IntersectRay(mapId, ray, distance, returnOnFirstHit: true))
+ {
+ hit = result;
+ return true;
+ }
+
+ hit = default;
+ return false;
+ }
+
+ private bool TryGetFirstRayHit(MapId mapId, CollisionRay ray, float distance, out RayCastResults hit, ref int raycastBudget)
+ {
+ if (raycastBudget <= 0)
+ {
+ hit = default;
+ return false;
+ }
+
+ raycastBudget--;
+
+ foreach (var result in _physics.IntersectRay(mapId, ray, distance, returnOnFirstHit: true))
+ {
+ hit = result;
+ return true;
+ }
+
+ hit = default;
+ return false;
+ }
+
+ private bool TryGetContactNormal(EntityUid uid, EntityUid target, out Vector2 normal)
+ {
+ normal = Vector2.Zero;
+
+ if (!TryComp(uid, out FixturesComponent? fixtures))
+ return false;
+
+ var contacts = _physics.GetContacts((uid, fixtures));
+ while (contacts.MoveNext(out var contact))
+ {
+ if (contact == null)
+ continue;
+
+ var bodyA = contact.BodyA;
+ var bodyB = contact.BodyB;
+ if (bodyA == null || bodyB == null)
+ continue;
+
+ if (bodyA.Owner != uid && bodyB.Owner != uid)
+ continue;
+
+ var other = bodyA.Owner == uid ? bodyB.Owner : bodyA.Owner;
+ if (other != target)
+ continue;
+
+ var (posA, rotA) = _transform.GetWorldPositionRotation(bodyA.Owner);
+ var (posB, rotB) = _transform.GetWorldPositionRotation(bodyB.Owner);
+ var transformA = new Robust.Shared.Physics.Transform(posA, rotA);
+ var transformB = new Robust.Shared.Physics.Transform(posB, rotB);
+ contact.GetWorldManifold(transformA, transformB, out var contactNormal);
+
+ if (bodyA.Owner == uid)
+ contactNormal = -contactNormal;
+
+ normal = contactNormal.Normalized();
+ return true;
+ }
+
+ return false;
+ }
+
+ private Vector2 CalculateDynamicBounce(EntityUid projectile, PhysicsComponent physics, EntityUid target, float speed, Vector2 normal)
+ {
+ var reflect = Vector2.Reflect(physics.LinearVelocity.Normalized(), normal);
+ if (target == EntityUid.Invalid || !Exists(target)) return reflect * speed;
+
+ var toTarget = (_transform.GetWorldPosition(target) - _transform.GetWorldPosition(projectile)).Normalized();
+ return (reflect * 0.3f + toTarget * 0.7f).Normalized() * speed;
+ }
+}
diff --git a/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverComponent.cs b/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverComponent.cs
new file mode 100644
index 000000000000..61e8c0b0aefd
--- /dev/null
+++ b/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverComponent.cs
@@ -0,0 +1,68 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Content.Shared.Actions;
+
+namespace Content.Pirate.Shared._JustDecor.Weapons.SmartRevolver;
+
+///
+/// Компонент для розумного револьвера, що може вибирати цілі і стріляти рекошетними патронами.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class SmartRevolverComponent : Component
+{
+ ///
+ /// Вибрана ціль.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityUid? SelectedTarget;
+
+ ///
+ /// Максимальна дистанція для вибору цілі.
+ ///
+ [DataField]
+ public float MaxTargetDistance = 50f;
+
+ ///
+ /// Чи показувати візуальний превью траєкторії (опціональна функція).
+ ///
+ [DataField]
+ public bool ShowTrajectory = false;
+
+ ///
+ /// Мінімальна кількість відскоків для куль, випущених з цієї зброї.
+ ///
+ [DataField]
+ public int MinRicochets = 1;
+
+ ///
+ /// Максимальна кількість відскоків для куль, випущених з цієї зброї.
+ ///
+ [DataField]
+ public int MaxRicochets = 4;
+
+ ///
+ /// Список всіх доступних цілей для циклу.
+ ///
+ [ViewVariables]
+ public List AvailableTargets = new();
+
+ ///
+ /// Поточний індекс у списку доступних цілей.
+ ///
+ [ViewVariables]
+ public int CurrentTargetIndex = 0;
+
+ ///
+ /// Дія для циклу по цілях.
+ ///
+ [DataField]
+ public EntityUid? CycleTargetAction;
+}
+
+///
+/// Івент для циклу по цілях.
+///
+[DataDefinition]
+public sealed partial class CycleSmartRevolverTargetEvent : InstantActionEvent
+{
+}
diff --git a/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverMessages.cs b/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverMessages.cs
new file mode 100644
index 000000000000..90dd8ed948c6
--- /dev/null
+++ b/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverMessages.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.Serialization;
+using Robust.Shared.GameObjects;
+
+namespace Content.Pirate.Shared._JustDecor.Weapons.SmartRevolver;
+
+///
+/// Client sends this to server to request setting a target.
+///
+[Serializable, NetSerializable]
+public sealed class SmartRevolverSetTargetMessage : EntityEventArgs
+{
+ public NetEntity Target;
+
+ public SmartRevolverSetTargetMessage(NetEntity target)
+ {
+ Target = target;
+ }
+}
diff --git a/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverSystem.cs b/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverSystem.cs
new file mode 100644
index 000000000000..253576dfc10e
--- /dev/null
+++ b/Content.Pirate.Shared/_JustDecor/Weapons/SmartRevolver/SmartRevolverSystem.cs
@@ -0,0 +1,286 @@
+using Content.Pirate.Shared._JustDecor.Weapons.Ranged;
+using Content.Shared.Actions;
+using Content.Shared.CombatMode;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Popups;
+using Content.Shared.Weapons.Ranged.Components;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
+using Content.Shared.Projectiles;
+using Content.Shared.Verbs;
+using Content.Shared.Examine;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Pirate.Shared._JustDecor.Weapons.SmartRevolver;
+
+///
+/// System that handles smart revolver target selection, cycling, and ricochet bullet creation.
+///
+public sealed class SmartRevolverSystem : EntitySystem
+{
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedGunSystem _gun = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly ActionContainerSystem _actions = default!;
+ [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
+ [Dependency] private readonly Content.Shared.Hands.EntitySystems.SharedHandsSystem _hands = default!;
+ [Dependency] private readonly Robust.Shared.Random.IRobustRandom _random = default!;
+ [Dependency] private readonly ExamineSystemShared _examine = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnAmmoShot);
+ SubscribeLocalEvent>(OnAlternativeVerb);
+ SubscribeLocalEvent(OnCycleTarget);
+ SubscribeLocalEvent(OnGetItemActions);
+
+ SubscribeNetworkEvent(OnSetTargetMessage);
+ }
+
+ private void OnMapInit(EntityUid uid, SmartRevolverComponent component, MapInitEvent args)
+ {
+ _actions.EnsureAction(uid, ref component.CycleTargetAction, "ActionCycleSmartRevolverTarget");
+ }
+
+ private void OnGetItemActions(EntityUid uid, SmartRevolverComponent component, GetItemActionsEvent args)
+ {
+ args.AddAction(ref component.CycleTargetAction, "ActionCycleSmartRevolverTarget");
+ }
+
+ private void OnAlternativeVerb(EntityUid uid, SmartRevolverComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanInteract || !IsValidTarget(args.Target))
+ return;
+
+ args.Verbs.Add(new AlternativeVerb
+ {
+ Act = () => SetTarget(uid, component, args.Target, args.User),
+ Text = Loc.GetString("smart-revolver-target-selection"),
+ Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/scope.svg.192dpi.png")),
+ Priority = 100
+ });
+ }
+
+ private void OnSetTargetMessage(SmartRevolverSetTargetMessage msg, EntitySessionEventArgs args)
+ {
+ var user = args.SenderSession.AttachedEntity;
+ if (user == null || !_combatMode.IsInCombatMode(user.Value))
+ return;
+
+ if (!_hands.TryGetActiveItem(user.Value, out var heldEntity))
+ return;
+
+ if (!TryComp(heldEntity, out var component))
+ return;
+
+ var target = GetEntity(msg.Target);
+
+ // Логіка: якщо клікнути на ту саму ціль, очистити
+ if (component.SelectedTarget == target)
+ {
+ ClearTarget(heldEntity.Value, component, user.Value);
+ return;
+ }
+ if (msg.Target == NetEntity.Invalid || !IsValidTarget(target))
+ {
+ ClearTarget(heldEntity.Value, component, user.Value);
+ return;
+ }
+
+ var userPos = _transform.GetMapCoordinates(user.Value);
+ var targetPos = _transform.GetMapCoordinates(target);
+
+ if (targetPos.MapId != userPos.MapId)
+ {
+ ClearTarget(heldEntity.Value, component, user.Value);
+ return;
+ }
+
+ var distance = (targetPos.Position - userPos.Position).Length();
+ if (distance > component.MaxTargetDistance || !_examine.InRangeUnOccluded(user.Value, target, component.MaxTargetDistance))
+ {
+ ClearTarget(heldEntity.Value, component, user.Value);
+ return;
+ }
+
+ SetTarget(heldEntity.Value, component, target, user.Value);
+ }
+
+ private void OnAmmoShot(EntityUid uid, SmartRevolverComponent component, ref AmmoShotEvent args)
+ {
+ if (!TryComp(uid, out GunComponent? gun))
+ return;
+
+ var target = component.SelectedTarget ?? gun.Target;
+
+ if (target == null)
+ return;
+
+ if (!IsValidTarget(target.Value))
+ {
+ ClearTarget(uid, component, null);
+ return;
+ }
+
+ var revolverPos = _transform.GetMapCoordinates(uid);
+ var targetPos = _transform.GetMapCoordinates(target.Value);
+ if (targetPos.MapId != revolverPos.MapId ||
+ (targetPos.Position - revolverPos.Position).Length() > component.MaxTargetDistance)
+ {
+ ClearTarget(uid, component, null);
+ return;
+ }
+
+ foreach (var projectile in args.FiredProjectiles)
+ {
+ if (TryComp(projectile, out var proj))
+ {
+ proj.DeleteOnCollide = false;
+ Dirty(projectile, proj);
+ }
+
+ var ricochet = EnsureComp(projectile);
+ ricochet.Target = target;
+
+ ricochet.TargetBounces = _random.Next(component.MinRicochets, component.MaxRicochets + 1);
+ ricochet.MaxBounces = ricochet.TargetBounces;
+
+ ricochet.FollowPlannedPath = true;
+ Dirty(projectile, ricochet);
+ }
+ }
+
+ private void OnCycleTarget(EntityUid uid, SmartRevolverComponent component, CycleSmartRevolverTargetEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ if (!_net.IsServer)
+ return;
+
+ if (!args.Performer.IsValid())
+ return;
+
+ UpdateAvailableTargets(uid, component, args.Performer);
+
+ if (component.AvailableTargets.Count == 0)
+ {
+ _popup.PopupEntity(Loc.GetString("smart-revolver-no-valid-targets"), uid, PopupType.Medium);
+ args.Handled = true;
+ return;
+ }
+
+ component.CurrentTargetIndex = (component.CurrentTargetIndex + 1) % component.AvailableTargets.Count;
+ var newTarget = component.AvailableTargets[component.CurrentTargetIndex];
+
+ SetTarget(uid, component, newTarget, null);
+ args.Handled = true;
+ }
+
+ public void SetTarget(EntityUid revolverUid, SmartRevolverComponent component, EntityUid target, EntityUid? user)
+ {
+ component.SelectedTarget = target;
+ Dirty(revolverUid, component);
+
+ if (_net.IsServer)
+ {
+ var targetName = MetaData(target).EntityName;
+ var message = Loc.GetString("smart-revolver-target-set", ("target", targetName));
+
+ if (user != null && Exists(user.Value))
+ {
+ _popup.PopupEntity(message, revolverUid, user.Value, PopupType.Medium);
+ }
+ else
+ {
+ _popup.PopupEntity(message, revolverUid, PopupType.Medium);
+ }
+ }
+ }
+
+ public void ClearTarget(EntityUid revolverUid, SmartRevolverComponent component, EntityUid? user)
+ {
+ if (component.SelectedTarget == null)
+ return;
+
+ component.SelectedTarget = null;
+ component.AvailableTargets.Clear();
+ component.CurrentTargetIndex = 0;
+ Dirty(revolverUid, component);
+
+ if (_net.IsServer)
+ {
+ var message = Loc.GetString("smart-revolver-target-cleared");
+
+ if (user != null && Exists(user.Value))
+ {
+ _popup.PopupEntity(message, revolverUid, user.Value, PopupType.Small);
+ }
+ else
+ {
+ _popup.PopupEntity(message, revolverUid, PopupType.Small);
+ }
+ }
+ }
+
+ private void UpdateAvailableTargets(EntityUid revolverUid, SmartRevolverComponent component, EntityUid user)
+ {
+ component.AvailableTargets.Clear();
+
+ var revolverPos = _transform.GetMapCoordinates(revolverUid);
+ foreach (var uid in _lookup.GetEntitiesInRange(revolverPos, component.MaxTargetDistance))
+ {
+ if (uid == revolverUid || uid == user)
+ continue;
+
+ if (!IsValidCycleTarget(uid))
+ continue;
+
+ if (!_examine.InRangeUnOccluded(user, uid, component.MaxTargetDistance))
+ continue;
+
+ if (!TryComp(uid, out TransformComponent? xform))
+ continue;
+
+ var targetPos = _transform.GetMapCoordinates(uid, xform);
+ if (targetPos.MapId != revolverPos.MapId)
+ continue;
+
+ var distance = (targetPos.Position - revolverPos.Position).Length();
+ if (distance > component.MaxTargetDistance)
+ continue;
+
+ component.AvailableTargets.Add(uid);
+ }
+ }
+
+ private bool IsValidTarget(EntityUid target)
+ {
+ // Перевіряємо чи ціль існує
+ if (!Exists(target) || Deleted(target))
+ return false;
+
+ // Перевіряємо наявність MobStateComponent або DamageableComponent
+ return HasComp(target) ||
+ HasComp(target);
+ }
+
+ private bool IsValidCycleTarget(EntityUid target)
+ {
+ if (!Exists(target) || Deleted(target))
+ return false;
+
+ return HasComp(target) || HasComp(target);
+ }
+}
diff --git a/Resources/Locale/uk-UA/_Pirate/ui/smart-revolver.ftl b/Resources/Locale/uk-UA/_Pirate/ui/smart-revolver.ftl
new file mode 100644
index 000000000000..e0721db15583
--- /dev/null
+++ b/Resources/Locale/uk-UA/_Pirate/ui/smart-revolver.ftl
@@ -0,0 +1,4 @@
+smart-revolver-target-selection = Вибрати ціль розумного револьвера
+smart-revolver-no-valid-targets = Немає дійсних цілей у полі зору!
+smart-revolver-target-set = Ціль встановлена: { $target }
+smart-revolver-target-cleared = Ціль очищена
diff --git a/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Actions/weapon_actions.yml b/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Actions/weapon_actions.yml
new file mode 100644
index 000000000000..0c49c793aa98
--- /dev/null
+++ b/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Actions/weapon_actions.yml
@@ -0,0 +1,12 @@
+- type: entity
+ parent: BaseAction
+ id: ActionCycleSmartRevolverTarget
+ name: Прокрутка цілей
+ description: Нехай доля обирає наступну жертву.
+ components:
+ - type: Action
+ icon: Interface/VerbIcons/refresh.svg.192dpi.png
+ useDelay: 0.5
+ itemIconStyle: BigItem
+ - type: InstantAction
+ event: !type:CycleSmartRevolverTargetEvent
diff --git a/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Entities/ocelot_ammo.yml b/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Entities/ocelot_ammo.yml
new file mode 100644
index 000000000000..feee041a6fa9
--- /dev/null
+++ b/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Entities/ocelot_ammo.yml
@@ -0,0 +1,43 @@
+- type: entity
+ id: CartridgeSmartMagnum
+ name: ".45 магнум розумний картридж"
+ parent: BaseCartridgeMagnum
+ suffix: JustDecor
+ components:
+ - type: CartridgeAmmo
+ proto: BulletSmartMagnum
+ - type: Sprite
+ layers:
+ - state: base
+ map: [ "enum.AmmoVisualLayers.Base" ]
+ - state: tip
+ map: [ "enum.AmmoVisualLayers.Tip" ]
+ color: "#FFD700"
+
+- type: entity
+ id: BulletSmartMagnum
+ name: ".45 розумний магнум патрон"
+ suffix: JustDecor
+ parent: BulletMagnum
+ components:
+ - type: RicochetProjectile
+ steeringStrength: 3.0
+ speedBonusPerBounce: 8.0
+ - type: TimedDespawn
+ lifetime: 10.0
+ - type: Projectile
+ damage:
+ types:
+ Piercing: 35
+ Structural: 15
+ deleteOnCollide: false # We handle it in the system
+
+- type: entity
+ id: SpeedLoaderMagnumSmart
+ name: "speed loader (.45 магнум розумний)"
+ parent: SpeedLoaderMagnum
+ suffix: JustDecor
+ components:
+ - type: BallisticAmmoProvider
+ proto: CartridgeSmartMagnum
+ capacity: 8
diff --git a/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Entities/smart_revolver.yml b/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Entities/smart_revolver.yml
new file mode 100644
index 000000000000..3be68e102141
--- /dev/null
+++ b/Resources/Prototypes/_Pirate/_JustDecor/_BigBos/Entities/smart_revolver.yml
@@ -0,0 +1,42 @@
+- type: entity
+ name: "Single Action Army"
+ parent: [WeaponRevolverMateba, BaseCentcommContraband]
+ id: WeaponRevolverSingleActionArmy
+ suffix: JustDecor
+ description: "Найкращий пістолет, який коли-небудь виготовляли. Single Action Army. Вісім куль... Більш ніж достатньо, щоб вбити все, що рухається. Цей легендарний револьвер оснащений вдосконаленою системою наведення, яка дозволяє кулям відскакувати від стін і влучати у вибрані цілі. На стволі надпис \"Ocelot\"."
+ components:
+ - type: Sprite
+ sprite: Objects/Weapons/Guns/Revolvers/mateba.rsi
+ state: icon
+ color: "#FFD700" # Gold color for legendary status
+ - type: Clothing
+ sprite: Objects/Weapons/Guns/Revolvers/mateba.rsi
+ - type: Gun
+ fireRate: 2
+ soundGunshot:
+ path: /Audio/Weapons/Guns/Gunshots/revolver.ogg
+ params:
+ volume: 3
+ - type: RevolverAmmoProvider
+ whitelist:
+ tags:
+ - CartridgeMagnum
+ - SpeedLoaderMagnum
+ proto: CartridgeSmartMagnum
+ capacity: 8
+ chambers: [ True, True, True, True, True, True, True, True ]
+ ammoSlots: [ null, null, null, null, null, null, null, null ]
+ soundEject:
+ path: /Audio/Weapons/Guns/MagOut/revolver_magout.ogg
+ soundInsert:
+ path: /Audio/Weapons/Guns/MagIn/revolver_magin.ogg
+ - type: SmartRevolver
+ maxTargetDistance: 50
+ showTrajectory: false
+ minRicochets: 1
+ maxRicochets: 4
+ - type: StaticPrice
+ price: 10000
+ - type: Tag
+ tags:
+ - Sidearm