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