diff --git a/Content.Shared/Whistle/WhistleComponent.cs b/Content.Shared/Whistle/WhistleComponent.cs
new file mode 100644
index 00000000000000..5004711514f522
--- /dev/null
+++ b/Content.Shared/Whistle/WhistleComponent.cs
@@ -0,0 +1,25 @@
+using Robust.Shared.GameStates;
+using Content.Shared.Humanoid;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Whistle;
+
+///
+/// Spawn attached entity for entities in range with .
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class WhistleComponent : Component
+{
+ ///
+ /// Entity prototype to spawn
+ ///
+ [DataField]
+ public EntProtoId Effect = "WhistleExclamation";
+
+ ///
+ /// Range value.
+ ///
+ [DataField]
+ public float Distance = 0;
+}
diff --git a/Content.Shared/Whistle/WhistleSystem.cs b/Content.Shared/Whistle/WhistleSystem.cs
new file mode 100644
index 00000000000000..9db7ffa0bf0b79
--- /dev/null
+++ b/Content.Shared/Whistle/WhistleSystem.cs
@@ -0,0 +1,64 @@
+using Content.Shared.Coordinates;
+using Content.Shared.Humanoid;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Stealth.Components;
+using JetBrains.Annotations;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Whistle;
+
+public sealed class WhistleSystem : EntitySystem
+{
+ [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUseInHand);
+ }
+
+ private void ExclamateTarget(EntityUid target, WhistleComponent component)
+ {
+ SpawnAttachedTo(component.Effect, target.ToCoordinates());
+ }
+
+ public void OnUseInHand(EntityUid uid, WhistleComponent component, UseInHandEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ TryMakeLoudWhistle(uid, args.User, component);
+ args.Handled = true;
+ }
+
+ public bool TryMakeLoudWhistle(EntityUid uid, EntityUid owner, WhistleComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, false) || component.Distance <= 0)
+ return false;
+
+ MakeLoudWhistle(uid, owner, component);
+ return true;
+ }
+
+ private void MakeLoudWhistle(EntityUid uid, EntityUid owner, WhistleComponent component)
+ {
+ StealthComponent? stealth = null;
+
+ foreach (var iterator in
+ _entityLookup.GetEntitiesInRange(_transform.GetMapCoordinates(uid), component.Distance))
+ {
+ //Avoid pinging invisible entities
+ if (TryComp(iterator, out stealth) && stealth.Enabled)
+ continue;
+
+ //We don't want to ping user of whistle
+ if (iterator.Owner == owner)
+ continue;
+
+ ExclamateTarget(iterator, component);
+ }
+ }
+}
diff --git a/Resources/Audio/Items/Whistle/attributions.yml b/Resources/Audio/Items/Whistle/attributions.yml
new file mode 100644
index 00000000000000..25a3da91744e59
--- /dev/null
+++ b/Resources/Audio/Items/Whistle/attributions.yml
@@ -0,0 +1,11 @@
+- files:
+ - "whistle_1.ogg"
+ - "whistle_2.ogg"
+ - "whistle_3.ogg"
+ - "whistle_4.ogg"
+ - "whistle_5.ogg"
+ - "whistle_6.ogg"
+ - "whistle_7.ogg"
+ license: "CC0-1.0"
+ copyright: "User strongbot on freesound.org. Modified by Fahasor on github"
+ source: "https://freesound.org/people/strongbot/sounds/568995/"
\ No newline at end of file
diff --git a/Resources/Audio/Items/Whistle/whistle_1.ogg b/Resources/Audio/Items/Whistle/whistle_1.ogg
new file mode 100644
index 00000000000000..5951dc89cbbd3a
Binary files /dev/null and b/Resources/Audio/Items/Whistle/whistle_1.ogg differ
diff --git a/Resources/Audio/Items/Whistle/whistle_2.ogg b/Resources/Audio/Items/Whistle/whistle_2.ogg
new file mode 100644
index 00000000000000..8af0d9ef54725a
Binary files /dev/null and b/Resources/Audio/Items/Whistle/whistle_2.ogg differ
diff --git a/Resources/Audio/Items/Whistle/whistle_3.ogg b/Resources/Audio/Items/Whistle/whistle_3.ogg
new file mode 100644
index 00000000000000..f8b53be200ab48
Binary files /dev/null and b/Resources/Audio/Items/Whistle/whistle_3.ogg differ
diff --git a/Resources/Audio/Items/Whistle/whistle_4.ogg b/Resources/Audio/Items/Whistle/whistle_4.ogg
new file mode 100644
index 00000000000000..cecdbca65224e4
Binary files /dev/null and b/Resources/Audio/Items/Whistle/whistle_4.ogg differ
diff --git a/Resources/Audio/Items/Whistle/whistle_5.ogg b/Resources/Audio/Items/Whistle/whistle_5.ogg
new file mode 100644
index 00000000000000..feca849a5e7954
Binary files /dev/null and b/Resources/Audio/Items/Whistle/whistle_5.ogg differ
diff --git a/Resources/Audio/Items/Whistle/whistle_6.ogg b/Resources/Audio/Items/Whistle/whistle_6.ogg
new file mode 100644
index 00000000000000..8db768776f7f0c
Binary files /dev/null and b/Resources/Audio/Items/Whistle/whistle_6.ogg differ
diff --git a/Resources/Audio/Items/Whistle/whistle_7.ogg b/Resources/Audio/Items/Whistle/whistle_7.ogg
new file mode 100644
index 00000000000000..787463fe78c582
Binary files /dev/null and b/Resources/Audio/Items/Whistle/whistle_7.ogg differ
diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml
index 75f0cad84b8d75..227d8767e83cc7 100644
--- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml
+++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml
@@ -1,6 +1,7 @@
- type: vendingMachineInventory
id: SecTechInventory
startingInventory:
+ SecurityWhistle: 5
Handcuffs: 8
GrenadeFlashBang: 4
TearGasGrenade: 4
diff --git a/Resources/Prototypes/Entities/Effects/exclamation.yml b/Resources/Prototypes/Entities/Effects/exclamation.yml
new file mode 100644
index 00000000000000..cfe1cbc7f22e7f
--- /dev/null
+++ b/Resources/Prototypes/Entities/Effects/exclamation.yml
@@ -0,0 +1,39 @@
+- type: entity
+ id: Exclamation
+ name: exclamation
+ noSpawn: true
+ save: false
+ components:
+ - type: Transform
+ noRot: true
+ - type: Sprite
+ sprite: Structures/Storage/closet.rsi
+ drawdepth: Effects
+ noRot: true
+ layers:
+ - state: "cardboard_special"
+ - type: TimedDespawn
+ lifetime: 1
+ - type: Tag
+ tags:
+ - HideContextMenu
+
+
+- type: entity
+ id: WhistleExclamation
+ name: exclamation
+ noSpawn: true
+ components:
+ - type: Sprite
+ sprite: Structures/Storage/closet.rsi
+ state: cardboard_special
+ drawdepth: Effects
+ offset: 0, 1
+ noRot: true
+ - type: Transform
+ noRot: true
+ - type: TimedDespawn
+ lifetime: 0.8
+ - type: Tag
+ tags:
+ - HideContextMenu
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Fun/whistles.yml b/Resources/Prototypes/Entities/Objects/Fun/whistles.yml
new file mode 100644
index 00000000000000..4281f0c4d4f488
--- /dev/null
+++ b/Resources/Prototypes/Entities/Objects/Fun/whistles.yml
@@ -0,0 +1,30 @@
+- type: entity
+ abstract: true
+ parent: BaseItem
+ id: BaseWhistle
+ name: whistle
+ description: Someone forgot to turn off kettle?
+ components:
+ - type: Item
+ size: Tiny
+ - type: Clothing
+ quickEquip: false
+ slots: neck
+ - type: UseDelay
+ delay: 3
+ - type: EmitSoundOnUse
+ sound:
+ collection: BaseWhistle
+ - type: Whistle
+ distance: 5
+
+- type: entity
+ parent: BaseWhistle
+ id: SecurityWhistle
+ description: Sound of it make you feel fear.
+ components:
+ - type: Sprite
+ sprite: Objects/Fun/whistle.rsi
+ state: securityWhistle
+ - type: Item
+ sprite: Objects/Fun/whistle.rsi
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml
index 040aff42b456bb..ac7f053b631aca 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml
@@ -107,25 +107,4 @@
- type: EntityStorage
isCollidableWhenOpen: false
openOnMove: false
- airtight: false
-
-#Exclamation effect for box opening
-- type: entity
- id: Exclamation
- name: exclamation
- noSpawn: true
- save: false
- components:
- - type: Transform
- noRot: true
- - type: Sprite
- sprite: Structures/Storage/closet.rsi
- drawdepth: Effects
- noRot: true
- layers:
- - state: "cardboard_special"
- - type: TimedDespawn
- lifetime: 1
- - type: Tag
- tags:
- - HideContextMenu
+ airtight: false
\ No newline at end of file
diff --git a/Resources/Prototypes/SoundCollections/whistle.yml b/Resources/Prototypes/SoundCollections/whistle.yml
new file mode 100644
index 00000000000000..f7d0dc9a1da9f7
--- /dev/null
+++ b/Resources/Prototypes/SoundCollections/whistle.yml
@@ -0,0 +1,10 @@
+- type: soundCollection
+ id: BaseWhistle
+ files:
+ - /Audio/Items/Whistle/whistle_1.ogg
+ - /Audio/Items/Whistle/whistle_2.ogg
+ - /Audio/Items/Whistle/whistle_3.ogg
+ - /Audio/Items/Whistle/whistle_4.ogg
+ - /Audio/Items/Whistle/whistle_5.ogg
+ - /Audio/Items/Whistle/whistle_6.ogg
+ - /Audio/Items/Whistle/whistle_7.ogg
diff --git a/Resources/Textures/Objects/Fun/whistle.rsi/meta.json b/Resources/Textures/Objects/Fun/whistle.rsi/meta.json
new file mode 100644
index 00000000000000..59159ff6170f9d
--- /dev/null
+++ b/Resources/Textures/Objects/Fun/whistle.rsi/meta.json
@@ -0,0 +1,14 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Made by Foleps (discord)",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "securityWhistle"
+ }
+ ]
+}
diff --git a/Resources/Textures/Objects/Fun/whistle.rsi/securityWhistle.png b/Resources/Textures/Objects/Fun/whistle.rsi/securityWhistle.png
new file mode 100644
index 00000000000000..81e43549b1d1e6
Binary files /dev/null and b/Resources/Textures/Objects/Fun/whistle.rsi/securityWhistle.png differ