diff --git a/Content.Client/Jobs/ClownSpecial.cs b/Content.Client/Jobs/ClownSpecial.cs new file mode 100644 index 0000000000..8c4af7de1d --- /dev/null +++ b/Content.Client/Jobs/ClownSpecial.cs @@ -0,0 +1,12 @@ +using Content.Server.Jobs; +using JetBrains.Annotations; + +namespace Content.Client.Jobs +{ + [UsedImplicitly] + public sealed class ClownSpecial : JobSpecial + { + // Dummy class that exists solely to avoid an exception on the client, + // but allow the server-side counterpart to exist. + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/ClumsyComponent.cs b/Content.Server/GameObjects/Components/Mobs/ClumsyComponent.cs new file mode 100644 index 0000000000..5e55a21a3e --- /dev/null +++ b/Content.Server/GameObjects/Components/Mobs/ClumsyComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.Components.Mobs +{ + /// + /// A simple clumsy tag-component. + /// + [RegisterComponent] + public class ClumsyComponent : Component + { + public override string Name => "Clumsy"; + } +} diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerRangedBarrelComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerRangedBarrelComponent.cs index 024a4af52c..907557534d 100644 --- a/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerRangedBarrelComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Barrels/ServerRangedBarrelComponent.cs @@ -27,6 +27,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization; using Robust.Shared.Utility; +using Content.Server.Interfaces; namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels { @@ -41,6 +42,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels #pragma warning disable 649 [Dependency] private IGameTiming _gameTiming; [Dependency] private IRobustRandom _robustRandom; + [Dependency] private readonly IServerNotifyManager _notifyManager; #pragma warning restore 649 public override FireRateSelector FireRateSelector => _fireRateSelector; @@ -235,6 +237,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels recoilComponent.Kick(-angle.ToVec() * 0.15f); } + // This section probably needs tweaking so there can be caseless hitscan etc. if (projectile.TryGetComponent(out HitscanComponent hitscan)) { diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/ServerRangedWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/ServerRangedWeaponComponent.cs index c66447333d..577549fa50 100644 --- a/Content.Server/GameObjects/Components/Weapon/Ranged/ServerRangedWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Ranged/ServerRangedWeaponComponent.cs @@ -2,17 +2,27 @@ using System; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels; +using Content.Shared.GameObjects; using Content.Shared.GameObjects.Components.Weapons.Ranged; using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.Audio; using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Random; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Players; +using Robust.Shared.Random; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Weapon.Ranged { @@ -21,6 +31,11 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged { private TimeSpan _lastFireTime; + [ViewVariables(VVAccess.ReadWrite)] + public bool ClumsyCheck { get; set; } + [ViewVariables(VVAccess.ReadWrite)] + public float ClumsyExplodeChance { get; set; } + public Func WeaponCanFireHandler; public Func UserCanFireHandler; public Action FireHandler; @@ -54,6 +69,14 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged return (UserCanFireHandler == null || UserCanFireHandler(user)) && ActionBlockerSystem.CanAttack(user); } + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(this, p => p.ClumsyCheck, "clumsyCheck", true); + serializer.DataField(this, p => p.ClumsyExplodeChance, "clumsyExplodeChance", 0.5f); + } + public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession session = null) { base.HandleNetworkMessage(message, channel, session); @@ -106,6 +129,35 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged } _lastFireTime = curTime; + + if (ClumsyCheck && + user.HasComponent() && + IoCManager.Resolve().Prob(ClumsyExplodeChance)) + { + var soundSystem = EntitySystem.Get(); + soundSystem.PlayAtCoords("/Audio/Items/bikehorn.ogg", + Owner.Transform.GridPosition, AudioParams.Default, 5); + + soundSystem.PlayAtCoords("/Audio/Weapons/Guns/Gunshots/bang.ogg", + Owner.Transform.GridPosition, AudioParams.Default, 5); + + if (user.TryGetComponent(out DamageableComponent health)) + { + health.TakeDamage(DamageType.Brute, 10); + health.TakeDamage(DamageType.Heat, 5); + } + + if (user.TryGetComponent(out StunnableComponent stun)) + { + stun.Paralyze(3f); + } + + user.PopupMessage(user, Loc.GetString("The gun blows up in your face!")); + + Owner.Delete(); + return; + } + FireHandler?.Invoke(user, coordinates); } diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 156fc374af..8065da1507 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -777,6 +777,7 @@ namespace Content.Server.GameTicking AddManifestEntry(character.Name, jobId); AddSpawnedPosition(jobId); EquipIdCard(mob, character.Name, jobPrototype); + jobPrototype.Special?.AfterEquip(mob); } private void EquipIdCard(IEntity mob, string characterName, JobPrototype jobPrototype) diff --git a/Content.Server/Jobs/ClownSpecial.cs b/Content.Server/Jobs/ClownSpecial.cs new file mode 100644 index 0000000000..72dc3000f5 --- /dev/null +++ b/Content.Server/Jobs/ClownSpecial.cs @@ -0,0 +1,18 @@ +using Content.Server.GameObjects.Components.Mobs; +using JetBrains.Annotations; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.Jobs +{ + // Used by clown job def. + [UsedImplicitly] + public sealed class ClownSpecial : JobSpecial + { + public override void AfterEquip(IEntity mob) + { + base.AfterEquip(mob); + + mob.AddComponent(); + } + } +} diff --git a/Content.Shared/Roles/JobPrototype.cs b/Content.Shared/Roles/JobPrototype.cs index cebac42c9f..298a33530a 100644 --- a/Content.Shared/Roles/JobPrototype.cs +++ b/Content.Shared/Roles/JobPrototype.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; +using Content.Server.Jobs; using Robust.Shared.Localization; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; +using Robust.Shared.Serialization; using YamlDotNet.RepresentationModel; namespace Content.Shared.Jobs @@ -41,44 +41,25 @@ namespace Content.Shared.Jobs public string Icon { get; private set; } + public JobSpecial Special { get; private set; } + public IReadOnlyCollection Department { get; private set; } public IReadOnlyCollection Access { get; private set; } public void LoadFrom(YamlMappingNode mapping) { - ID = mapping.GetNode("id").AsString(); - Name = Loc.GetString(mapping.GetNode("name").ToString()); - StartingGear = mapping.GetNode("startingGear").ToString(); - Department = mapping.GetNode("department").AllNodes.Select(i => i.ToString()).ToList(); - TotalPositions = mapping.GetNode("positions").AsInt(); + var srz = YamlObjectSerializer.NewReader(mapping); + ID = srz.ReadDataField("id"); + Name = Loc.GetString(srz.ReadDataField("name")); + StartingGear = srz.ReadDataField("startingGear"); + Department = srz.ReadDataField>("department"); + TotalPositions = srz.ReadDataField("positions"); - if (mapping.TryGetNode("spawnPositions", out var positionsNode)) - { - SpawnPositions = positionsNode.AsInt(); - } - else - { - SpawnPositions = TotalPositions; - } - - if (mapping.TryGetNode("head", out var headNode)) - { - IsHead = headNode.AsBool(); - } - - if (mapping.TryGetNode("access", out YamlSequenceNode accessNode)) - { - Access = accessNode.Select(i => i.ToString()).ToList(); - } - else - { - Access = Array.Empty(); - } - - if (mapping.TryGetNode("icon", out var iconNode)) - { - Icon = iconNode.AsString(); - } + srz.DataField(this, p => p.SpawnPositions, "spawnPositions", TotalPositions); + srz.DataField(this, p => p.IsHead, "head", false); + srz.DataField(this, p => p.Access, "access", Array.Empty()); + srz.DataField(this, p => p.Icon, "icon", null); + srz.DataField(this, p => p.Special, "special", null); } } } diff --git a/Content.Shared/Roles/JobSpecial.cs b/Content.Shared/Roles/JobSpecial.cs new file mode 100644 index 0000000000..6b77fa5400 --- /dev/null +++ b/Content.Shared/Roles/JobSpecial.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Serialization; + +namespace Content.Server.Jobs +{ + /// + /// Provides special hooks for when jobs get spawned in/equipped. + /// + public abstract class JobSpecial : IExposeData + { + void IExposeData.ExposeData(ObjectSerializer serializer) + { + ExposeData(serializer); + } + + protected virtual void ExposeData(ObjectSerializer serializer) + { + } + + public virtual void AfterEquip(IEntity mob) + { + + } + } +} diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml index 63f503d0f0..6070cdbf40 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml @@ -8,6 +8,7 @@ icon: "Clown" access: - Theatre + special: !type:ClownSpecial {} - type: startingGear id: ClownGear