diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index a40b695885..12aa84413b 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -345,6 +345,7 @@ namespace Content.Client.Entry
"Artifact",
"RandomArtifactSprite",
"EnergySword",
+ "MeleeSound",
"DoorRemote",
"InteractionPopup",
"HealthAnalyzer",
diff --git a/Content.Server/Weapon/Melee/Components/MeleeSoundComponent.cs b/Content.Server/Weapon/Melee/Components/MeleeSoundComponent.cs
new file mode 100644
index 0000000000..0a338b16de
--- /dev/null
+++ b/Content.Server/Weapon/Melee/Components/MeleeSoundComponent.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Sound;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
+
+namespace Content.Server.Weapon.Melee.Components;
+
+///
+/// Plays the specified sound upon receiving damage of the specified type.
+///
+[RegisterComponent]
+public sealed class MeleeSoundComponent : Component
+{
+ ///
+ /// Specified sounds to apply when the entity takes damage with the specified group.
+ /// Will fallback to defaults if none specified.
+ ///
+ [DataField("soundGroups",
+ customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
+ public Dictionary? SoundGroups;
+
+ ///
+ /// Specified sounds to apply when the entity takes damage with the specified type.
+ /// Will fallback to defaults if none specified.
+ ///
+ [DataField("soundTypes",
+ customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
+ public Dictionary? SoundTypes;
+
+ ///
+ /// Sound that plays if no damage is done.
+ ///
+ [DataField("noDamageSound")] public SoundSpecifier? NoDamageSound;
+}
diff --git a/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs b/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs
index cf1786fe9e..6841c91b3a 100644
--- a/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs
+++ b/Content.Server/Weapon/Melee/Components/MeleeWeaponComponent.cs
@@ -9,7 +9,7 @@ namespace Content.Server.Weapon.Melee.Components
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("hitSound")]
- public SoundSpecifier HitSound { get; set; } = new SoundCollectionSpecifier("GenericHit");
+ public SoundSpecifier? HitSound;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("missSound")]
diff --git a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs
index 712a8212f8..98f1ed2f2e 100644
--- a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs
+++ b/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs
@@ -10,8 +10,8 @@ using Content.Shared.Damage;
using Content.Shared.Sound;
using Content.Shared.Audio;
using Content.Shared.Database;
+using Content.Shared.FixedPoint;
using Content.Shared.Hands;
-using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Audio;
@@ -19,18 +19,22 @@ using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Weapon.Melee
{
public sealed class MeleeWeaponSystem : EntitySystem
{
- [Dependency] private IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
- [Dependency] private SolutionContainerSystem _solutionsSystem = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionsSystem = default!;
[Dependency] private readonly AdminLogSystem _logSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
+ private const float DamagePitchVariation = 0.15f;
+
public override void Initialize()
{
base.Initialize();
@@ -73,7 +77,7 @@ namespace Content.Server.Weapon.Melee
if (curTime < comp.CooldownEnd || args.Target == null)
return;
- var location = EntityManager.GetComponent(args.User).Coordinates;
+ var location = Transform(args.User).Coordinates;
var diff = args.ClickLocation.ToMapPos(EntityManager) - location.ToMapPos(EntityManager);
var angle = Angle.FromWorldVec(diff);
@@ -103,19 +107,12 @@ namespace Content.Server.Weapon.Melee
$"{ToPrettyString(args.User):user} melee attacked {ToPrettyString(args.Target.Value):target} using {ToPrettyString(args.Used):used} and dealt {damageResult.Total:damage} damage");
}
- if (hitEvent.HitSoundOverride != null)
- {
- SoundSystem.Play(Filter.Pvs(owner), hitEvent.HitSoundOverride.GetSound(), target, AudioHelpers.WithVariation(0.25f));
- }
- else
- {
- SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), target);
- }
+ PlayHitSound(target, GetHighestDamageSound(modifiedDamage), hitEvent.HitSoundOverride, comp.HitSound);
}
}
else
{
- SoundSystem.Play(Filter.Pvs(owner), comp.MissSound.GetSound(), args.User);
+ SoundSystem.Play(Filter.Pvs(owner, entityManager: EntityManager), comp.MissSound.GetSound(), args.User);
return;
}
@@ -161,24 +158,20 @@ namespace Content.Server.Weapon.Melee
if (!hitEvent.Handled)
{
+ var modifiedDamage = DamageSpecifier.ApplyModifierSets(comp.Damage + hitEvent.BonusDamage, hitEvent.ModifiersList);
+
if (entities.Count != 0)
{
- if (hitEvent.HitSoundOverride != null)
- {
- SoundSystem.Play(Filter.Pvs(owner), hitEvent.HitSoundOverride.GetSound(), Transform(entities.First()).Coordinates);
- }
- else
- {
- SoundSystem.Play(Filter.Pvs(owner), comp.HitSound.GetSound(), Transform(entities.First()).Coordinates);
- }
+ var target = entities.First();
+ TryComp(target, out var meleeWeapon);
+
+ PlayHitSound(target, GetHighestDamageSound(modifiedDamage), hitEvent.HitSoundOverride, meleeWeapon?.HitSound);
}
else
{
SoundSystem.Play(Filter.Pvs(owner), comp.MissSound.GetSound(), Transform(args.User).Coordinates);
}
- var modifiedDamage = DamageSpecifier.ApplyModifierSets(comp.Damage + hitEvent.BonusDamage, hitEvent.ModifiersList);
-
foreach (var entity in hitEntities)
{
RaiseLocalEvent(entity, new AttackedEvent(args.Used, args.User, args.ClickLocation));
@@ -203,6 +196,89 @@ namespace Content.Server.Weapon.Melee
RaiseLocalEvent(owner, new RefreshItemCooldownEvent(comp.LastAttackTime, comp.CooldownEnd), false);
}
+ private string? GetHighestDamageSound(DamageSpecifier modifiedDamage)
+ {
+ var groups = modifiedDamage.GetDamagePerGroup(_protoManager);
+
+ // Use group if it's exclusive, otherwise fall back to type.
+ if (groups.Count == 1)
+ {
+ return groups.Keys.First();
+ }
+
+ var highestDamage = FixedPoint2.Zero;
+ string? highestDamageType = null;
+
+ foreach (var (type, damage) in modifiedDamage.DamageDict)
+ {
+ if (damage <= highestDamage) continue;
+ highestDamageType = type;
+ }
+
+ return highestDamageType;
+ }
+
+ private void PlayHitSound(EntityUid target, string? type, SoundSpecifier? hitSoundOverride, SoundSpecifier? hitSound)
+ {
+ var playedSound = false;
+
+ // Play sound based off of highest damage type.
+ if (TryComp(target, out var damageSoundComp))
+ {
+ if (type == null && damageSoundComp.NoDamageSound != null)
+ {
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), damageSoundComp.NoDamageSound.GetSound(), target, AudioHelpers.WithVariation(DamagePitchVariation));
+ playedSound = true;
+ }
+ else if (type != null && damageSoundComp.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
+ {
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), damageSoundType!.GetSound(), target, AudioHelpers.WithVariation(DamagePitchVariation));
+ playedSound = true;
+ }
+ else if (type != null && damageSoundComp.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
+ {
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), damageSoundGroup!.GetSound(), target, AudioHelpers.WithVariation(DamagePitchVariation));
+ playedSound = true;
+ }
+ }
+
+ // Use weapon sounds if the thing being hit doesn't specify its own sounds.
+ if (!playedSound)
+ {
+ if (hitSoundOverride != null)
+ {
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), hitSoundOverride.GetSound(), target, AudioHelpers.WithVariation(DamagePitchVariation));
+ playedSound = true;
+ }
+ else if (hitSound != null)
+ {
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), hitSound.GetSound(), target);
+ playedSound = true;
+ }
+ }
+
+ // Fallback to generic sounds.
+ if (!playedSound)
+ {
+ switch (type)
+ {
+ // Unfortunately heat returns caustic group so can't just use the damagegroup in that instance.
+ case "Burn":
+ case "Heat":
+ case "Cold":
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), "/Audio/Items/welder.ogg", target);
+ break;
+ // No damage, fallback to tappies
+ case null:
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), "/Audio/Weapons/tap.ogg", target);
+ break;
+ case "Brute":
+ SoundSystem.Play(Filter.Pvs(target, entityManager: EntityManager), "/Audio/Weapons/smash.ogg", target);
+ break;
+ }
+ }
+ }
+
private HashSet ArcRayCast(Vector2 position, Angle angle, float arcWidth, float range, MapId mapId, EntityUid ignore)
{
var widthRad = Angle.FromDegrees(arcWidth);
diff --git a/Resources/Audio/Weapons/dodgeball.ogg b/Resources/Audio/Weapons/dodgeball.ogg
new file mode 100644
index 0000000000..878d923a8d
Binary files /dev/null and b/Resources/Audio/Weapons/dodgeball.ogg differ
diff --git a/Resources/Audio/Weapons/grille_hit.ogg b/Resources/Audio/Weapons/grille_hit.ogg
new file mode 100644
index 0000000000..0dc6c33367
Binary files /dev/null and b/Resources/Audio/Weapons/grille_hit.ogg differ
diff --git a/Resources/Audio/Weapons/licenses.txt b/Resources/Audio/Weapons/licenses.txt
new file mode 100644
index 0000000000..747730fba5
--- /dev/null
+++ b/Resources/Audio/Weapons/licenses.txt
@@ -0,0 +1,7 @@
+dodgeball.ogg taken from https://github.com/tgstation/tgstation/blob/5d264fbea0124e5af511af3fed24203e196d108b/sound/items/dodgeball.ogg under CC BY-SA 3.0
+
+grille_hit.ogg taken from https://github.com/tgstation/tgstation/blob/803ca4537df35cf252b056d8460d510be8a4f353/sound/effects/grillehit.ogg under CC BY-SA 3.0
+
+slash.ogg taken from https://github.com/tgstation/tgstation/blob/5d264fbea0124e5af511af3fed24203e196d108b/sound/weapons/slash.ogg under CC BY-SA 3.0
+
+tap.ogg taken from https://github.com/tgstation/tgstation/blob/803ca4537df35cf252b056d8460d510be8a4f353/sound/weapons/tap.ogg under CC BY-SA 3.0
\ No newline at end of file
diff --git a/Resources/Audio/Weapons/slash.ogg b/Resources/Audio/Weapons/slash.ogg
new file mode 100644
index 0000000000..ad357891eb
Binary files /dev/null and b/Resources/Audio/Weapons/slash.ogg differ
diff --git a/Resources/Audio/Weapons/tap.ogg b/Resources/Audio/Weapons/tap.ogg
new file mode 100644
index 0000000000..711cb0ac38
Binary files /dev/null and b/Resources/Audio/Weapons/tap.ogg differ
diff --git a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml
index 69649d4fe7..5ec243612f 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/kudzu.yml
@@ -7,6 +7,11 @@
snap:
- Wall
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Weapons/slash.ogg"
- type: Sprite
sprite: Objects/Misc/kudzu.rsi
state: kudzu_11
diff --git a/Resources/Prototypes/Entities/Objects/Power/lights.yml b/Resources/Prototypes/Entities/Objects/Power/lights.yml
index 7abb8e5c1f..39949c5d96 100644
--- a/Resources/Prototypes/Entities/Objects/Power/lights.yml
+++ b/Resources/Prototypes/Entities/Objects/Power/lights.yml
@@ -3,6 +3,11 @@
id: BaseLightbulb
abstract: true
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: Sprite
netsync: false
sprite: Objects/Power/light_bulb.rsi
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
index 2ff88c46cb..2e420304a0 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
@@ -13,7 +13,7 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/engineering.rsi
-
+
- type: entity
parent: Airlock
id: AirlockAtmospherics
@@ -21,7 +21,7 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Standard/atmospherics.rsi
-
+
- type: entity
parent: Airlock
id: AirlockCargo
@@ -85,6 +85,11 @@
parent: Airlock
name: glass airlock
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: Door
occludes: false
- type: Occluder
@@ -116,7 +121,7 @@
sprite: Structures/Doors/Airlocks/Glass/engineering.rsi
- type: PaintableAirlock
group: Glass
-
+
- type: entity
parent: AirlockGlass
id: AirlockAtmosphericsGlass
@@ -125,7 +130,7 @@
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/atmospherics.rsi
- type: PaintableAirlock
- group: Glass
+ group: Glass
- type: entity
parent: AirlockGlass
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
index caad086b62..c2adffdca7 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
@@ -4,6 +4,11 @@
name: airlock
description: It opens, it closes, and maybe crushes you.
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Weapons/smash.ogg"
- type: InteractionOutline
- type: Sprite
netsync: false
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
index 661e4fafa9..47c77070c1 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
@@ -5,6 +5,11 @@
placement:
mode: SnapgridCenter
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: InteractionOutline
- type: Physics
- type: Fixtures
diff --git a/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml b/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml
index bacce5ec45..593a1efc62 100644
--- a/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml
+++ b/Resources/Prototypes/Entities/Structures/Furniture/toilet.yml
@@ -5,6 +5,11 @@
parent: SeatBase
description: The HT-451, a torque rotation-based, waste disposal unit for small matter. This one seems remarkably clean.
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Weapons/slash.ogg"
- type: Anchorable
- type: Sprite
sprite: Structures/Furniture/toilet.rsi
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml
index 69c555d729..08a5a1c43b 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml
@@ -6,6 +6,11 @@
placement:
mode: SnapgridCenter
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: Construction
graph: Computer
node: computer
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml
index a9fb711db1..5fa17d3641 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml
@@ -5,6 +5,11 @@
placement:
mode: SnapgridCenter
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: Clickable
- type: InteractionOutline
- type: Transform
diff --git a/Resources/Prototypes/Entities/Structures/Storage/storage.yml b/Resources/Prototypes/Entities/Structures/Storage/storage.yml
index 7b75330bf3..229986682a 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/storage.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/storage.yml
@@ -4,6 +4,11 @@
name: rack
description: A rack for storing things on.
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Weapons/dodgeball.ogg"
- type: Construction
graph: Rack
node: Rack
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml
index 407293aea4..00a3e2c876 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml
@@ -3,6 +3,11 @@
parent: BaseStructure
name: bar sign
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: WallMount
- type: Sprite
drawdepth: Objects
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml
index 3d75006e41..84760fec9c 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml
@@ -3,6 +3,11 @@
name: fire axe cabinet
description: There is a small label that reads "For Emergency use only" along with details for safe use of the axe. As if.
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: WallMount
- type: Clickable
- type: InteractionOutline
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml
index ed670d3ea8..0262610616 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml
@@ -4,6 +4,11 @@
description: "An unpowered light."
suffix: Unpowered
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: Transform
anchored: true
- type: Clickable
diff --git a/Resources/Prototypes/Entities/Structures/Walls/grille.yml b/Resources/Prototypes/Entities/Structures/Walls/grille.yml
index 6aabe2102c..ed31c9262b 100644
--- a/Resources/Prototypes/Entities/Structures/Walls/grille.yml
+++ b/Resources/Prototypes/Entities/Structures/Walls/grille.yml
@@ -4,6 +4,11 @@
name: grille
description: A flimsy framework of iron rods.
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Weapons/grille_hit.ogg"
- type: Tag
tags:
- RCDDeconstructWhitelist
diff --git a/Resources/Prototypes/Entities/Structures/Windows/window.yml b/Resources/Prototypes/Entities/Structures/Windows/window.yml
index 0969147bb5..ea58cf9a32 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/window.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/window.yml
@@ -8,6 +8,11 @@
snap:
- Window
components:
+ - type: MeleeSound
+ soundGroups:
+ Brute:
+ path:
+ "/Audio/Effects/glass_hit.ogg"
- type: WallMount
arc: 360 # interact despite grilles
- type: Tag