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