From: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Date: Sun, 6 Jul 2025 16:54:20 +0000 (+0200)
Subject: Catchable items, playable basketball (#37702)
X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=22e3d533d3c38597423588b9f21446d434cc3221;p=space-station-14.git
Catchable items, playable basketball (#37702)
* catching
* fix
* improve
* fix linter
* cleanup
* fix prediction
* do the same here
* fix comment
---
diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
index b5d5100189..ac385040a9 100644
--- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
+++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
@@ -14,6 +14,7 @@ using Content.Shared.Forensics.Components;
using Content.Shared.HealthExaminable;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
+using Content.Shared.Random.Helpers;
using Content.Shared.Rejuvenate;
using Content.Shared.Speech.EntitySystems;
using Robust.Shared.Audio.Systems;
@@ -222,7 +223,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
// TODO: Replace with RandomPredicted once the engine PR is merged
// Use both the receiver and the damage causing entity for the seed so that we have different results for multiple attacks in the same tick
- var seed = HashCode.Combine((int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0);
+ var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0 });
var rand = new System.Random(seed);
var prob = Math.Clamp(totalFloat / 25, 0, 1);
if (totalFloat > 0 && rand.Prob(prob))
diff --git a/Content.Shared/Clumsy/ClumsyComponent.cs b/Content.Shared/Clumsy/ClumsyComponent.cs
index 6b013a5c2f..9e6e851c8c 100644
--- a/Content.Shared/Clumsy/ClumsyComponent.cs
+++ b/Content.Shared/Clumsy/ClumsyComponent.cs
@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Clumsy;
///
-/// A simple clumsy tag-component.
+/// Makes the entity clumsy, randomly failing some interactions and hurting themselves.
///
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ClumsyComponent : Component
@@ -48,11 +48,17 @@ public sealed partial class ClumsyComponent : Component
public TimeSpan GunShootFailStunTime = TimeSpan.FromSeconds(3);
///
- /// Stun time after failing to shoot a gun.
+ /// Damage taken after failing to shoot a gun.
///
[DataField, AutoNetworkedField]
public DamageSpecifier? GunShootFailDamage;
+ ///
+ /// Damage taken after failing to catch an item.
+ ///
+ [DataField, AutoNetworkedField]
+ public DamageSpecifier? CatchingFailDamage;
+
///
/// Noise to play after failing to shoot a gun. Boom!
///
@@ -77,6 +83,12 @@ public sealed partial class ClumsyComponent : Component
[DataField, AutoNetworkedField]
public bool ClumsyGuns = true;
+ ///
+ /// Whether or not to apply Clumsy to catching items.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool ClumsyCatching = true;
+
///
/// Whether or not to apply Clumsy to vaulting.
///
@@ -87,17 +99,23 @@ public sealed partial class ClumsyComponent : Component
/// Lets you define a new "failed" message for each event.
///
[DataField]
- public LocId HypoFailedMessage = "hypospray-component-inject-self-clumsy-message";
+ public LocId HypoFailedMessage = "clumsy-hypospray-fail-message";
+
+ [DataField]
+ public LocId GunFailedMessage = "clumsy-gun-fail-message";
+
+ [DataField]
+ public LocId CatchingFailedMessageSelf = "clumsy-catch-fail-message-user";
[DataField]
- public LocId GunFailedMessage = "gun-clumsy";
+ public LocId CatchingFailedMessageOthers = "clumsy-catch-fail-message-others";
[DataField]
- public LocId VaulingFailedMessageSelf = "bonkable-success-message-user";
+ public LocId VaulingFailedMessageSelf = "clumsy-vaulting-fail-message-user";
[DataField]
- public LocId VaulingFailedMessageOthers = "bonkable-success-message-others";
+ public LocId VaulingFailedMessageOthers = "clumsy-vaulting-fail-message-others";
[DataField]
- public LocId VaulingFailedMessageForced = "forced-bonkable-success-message";
+ public LocId VaulingFailedMessageForced = "clumsy-vaulting-fail-forced-message";
}
diff --git a/Content.Shared/Clumsy/ClumsySystem.cs b/Content.Shared/Clumsy/ClumsySystem.cs
index 348d99182a..9e0e82364f 100644
--- a/Content.Shared/Clumsy/ClumsySystem.cs
+++ b/Content.Shared/Clumsy/ClumsySystem.cs
@@ -6,10 +6,14 @@ using Content.Shared.Damage;
using Content.Shared.IdentityManagement;
using Content.Shared.Medical;
using Content.Shared.Popups;
+using Content.Shared.Random.Helpers;
using Content.Shared.Stunnable;
+using Content.Shared.Throwing;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -17,19 +21,20 @@ namespace Content.Shared.Clumsy;
public sealed class ClumsySystem : EntitySystem
{
- [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly INetManager _net = default!;
public override void Initialize()
{
SubscribeLocalEvent(BeforeHyposprayEvent);
SubscribeLocalEvent(BeforeDefibrillatorZapsEvent);
SubscribeLocalEvent(BeforeGunShotEvent);
+ SubscribeLocalEvent(OnCatchAttempt);
SubscribeLocalEvent(OnBeforeClimbEvent);
}
@@ -43,12 +48,15 @@ public sealed class ClumsySystem : EntitySystem
if (!ent.Comp.ClumsyHypo)
return;
- if (!_random.Prob(ent.Comp.ClumsyDefaultCheck))
+ // TODO: Replace with RandomPredicted once the engine PR is merged
+ var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
+ var rand = new System.Random(seed);
+ if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
return;
args.TargetGettingInjected = args.EntityUsingHypospray;
- args.InjectMessageOverride = "hypospray-component-inject-self-clumsy-message";
- _audio.PlayPvs(ent.Comp.ClumsySound, ent);
+ args.InjectMessageOverride = Loc.GetString(ent.Comp.HypoFailedMessage);
+ _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingHypospray);
}
private void BeforeDefibrillatorZapsEvent(Entity ent, ref SelfBeforeDefibrillatorZapsEvent args)
@@ -59,7 +67,10 @@ public sealed class ClumsySystem : EntitySystem
if (!ent.Comp.ClumsyDefib)
return;
- if (!_random.Prob(ent.Comp.ClumsyDefaultCheck))
+ // TODO: Replace with RandomPredicted once the engine PR is merged
+ var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
+ var rand = new System.Random(seed);
+ if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
return;
args.DefibTarget = args.EntityUsingDefib;
@@ -67,6 +78,37 @@ public sealed class ClumsySystem : EntitySystem
}
+ private void OnCatchAttempt(Entity ent, ref CatchAttemptEvent args)
+ {
+ // Clumsy people sometimes fail to catch items!
+
+ // checks if ClumsyCatching is false, if so, skips.
+ if (!ent.Comp.ClumsyCatching)
+ return;
+
+ // TODO: Replace with RandomPredicted once the engine PR is merged
+ var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Item).Id });
+ var rand = new System.Random(seed);
+ if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
+ return;
+
+ args.Cancelled = true; // fail to catch
+
+ if (ent.Comp.CatchingFailDamage != null)
+ _damageable.TryChangeDamage(ent, ent.Comp.CatchingFailDamage, origin: args.Item);
+
+ // Collisions don't work properly with PopupPredicted or PlayPredicted.
+ // So we make this server only.
+ if (_net.IsClient)
+ return;
+
+ var selfMessage = Loc.GetString(ent.Comp.CatchingFailedMessageSelf, ("item", ent.Owner), ("catcher", Identity.Entity(ent.Owner, EntityManager)));
+ var othersMessage = Loc.GetString(ent.Comp.CatchingFailedMessageOthers, ("item", ent.Owner), ("catcher", Identity.Entity(ent.Owner, EntityManager)));
+ _popup.PopupEntity(selfMessage, ent.Owner, ent.Owner);
+ _popup.PopupEntity(othersMessage, ent.Owner, Filter.PvsExcept(ent.Owner), true);
+ _audio.PlayPvs(ent.Comp.ClumsySound, ent);
+ }
+
private void BeforeGunShotEvent(Entity ent, ref SelfBeforeGunShotEvent args)
{
// Clumsy people sometimes can't shoot :(
@@ -78,7 +120,10 @@ public sealed class ClumsySystem : EntitySystem
if (args.Gun.Comp.ClumsyProof)
return;
- if (!_random.Prob(ent.Comp.ClumsyDefaultCheck))
+ // TODO: Replace with RandomPredicted once the engine PR is merged
+ var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(args.Gun).Id });
+ var rand = new System.Random(seed);
+ if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
return;
if (ent.Comp.GunShootFailDamage != null)
@@ -90,7 +135,7 @@ public sealed class ClumsySystem : EntitySystem
_audio.PlayPvs(ent.Comp.GunShootFailSound, ent);
_audio.PlayPvs(ent.Comp.ClumsySound, ent);
- _popup.PopupEntity(Loc.GetString("gun-clumsy"), ent, ent);
+ _popup.PopupEntity(Loc.GetString(ent.Comp.GunFailedMessage), ent, ent);
args.Cancel();
}
@@ -100,9 +145,9 @@ public sealed class ClumsySystem : EntitySystem
if (!ent.Comp.ClumsyVaulting)
return;
- // This event is called in shared, thats why it has all the extra prediction stuff.
- var rand = new System.Random((int)_timing.CurTick.Value);
-
+ // TODO: Replace with RandomPredicted once the engine PR is merged
+ var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
+ var rand = new System.Random(seed);
if (!_cfg.GetCVar(CCVars.GameTableBonk) && !rand.Prob(ent.Comp.ClumsyDefaultCheck))
return;
diff --git a/Content.Shared/Random/Helpers/SharedRandomExtensions.cs b/Content.Shared/Random/Helpers/SharedRandomExtensions.cs
index 42d92a9065..87e839b56f 100644
--- a/Content.Shared/Random/Helpers/SharedRandomExtensions.cs
+++ b/Content.Shared/Random/Helpers/SharedRandomExtensions.cs
@@ -184,5 +184,25 @@ namespace Content.Shared.Random.Helpers
// Shouldn't happen
throw new InvalidOperationException($"Invalid weighted pick for {prototype.ID}!");
}
+
+ ///
+ /// A very simple, deterministic djb2 hash function for generating a combined seed for the random number generator.
+ /// We can't use HashCode.Combine because that is initialized with a random value, creating different results on the server and client.
+ ///
+ ///
+ /// Combine the current game tick with a NetEntity Id in order to not get the same random result if this is called multiple times in the same tick.
+ ///
+ /// var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
+ ///
+ ///
+ public static int HashCodeCombine(List values)
+ {
+ int hash = 5381;
+ foreach (var value in values)
+ {
+ hash = (hash << 5) + hash + value;
+ }
+ return hash;
+ }
}
}
diff --git a/Content.Shared/Throwing/CatchAttemptEvent.cs b/Content.Shared/Throwing/CatchAttemptEvent.cs
new file mode 100644
index 0000000000..03206bdcd9
--- /dev/null
+++ b/Content.Shared/Throwing/CatchAttemptEvent.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.Throwing;
+
+///
+/// Raised on someone when they try to catch an item.
+///
+[ByRefEvent]
+public record struct CatchAttemptEvent(EntityUid Item, float CatchChance, bool Cancelled = false);
diff --git a/Content.Shared/Throwing/CatchableComponent.cs b/Content.Shared/Throwing/CatchableComponent.cs
new file mode 100644
index 0000000000..ce68374440
--- /dev/null
+++ b/Content.Shared/Throwing/CatchableComponent.cs
@@ -0,0 +1,39 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Throwing;
+
+///
+/// Allows this entity to be caught in your hands when someone else throws it at you.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class CatchableComponent : Component
+{
+ ///
+ /// If true this item can only be caught while in combat mode.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool RequireCombatMode;
+
+ ///
+ /// The chance of successfully catching.
+ ///
+ [DataField, AutoNetworkedField]
+ public float CatchChance = 1.0f;
+
+ ///
+ /// Optional whitelist for who can catch this item.
+ ///
+ ///
+ /// Example usecase: Only someone who knows martial arts can catch grenades.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityWhitelist? CatcherWhitelist;
+
+ ///
+ /// The sound to play when successfully catching.
+ ///
+ [DataField]
+ public SoundSpecifier? CatchSuccessSound;
+}
diff --git a/Content.Shared/Throwing/CatchableSystem.cs b/Content.Shared/Throwing/CatchableSystem.cs
new file mode 100644
index 0000000000..8f2fd355ba
--- /dev/null
+++ b/Content.Shared/Throwing/CatchableSystem.cs
@@ -0,0 +1,84 @@
+using Content.Shared.CombatMode;
+using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Popups;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Throwing;
+
+public sealed partial class CatchableSystem : EntitySystem
+{
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly ThrownItemSystem _thrown = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+
+ private EntityQuery _handsQuery;
+ private EntityQuery _combatModeQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnDoHit);
+
+ _handsQuery = GetEntityQuery();
+ _combatModeQuery = GetEntityQuery();
+ }
+
+ private void OnDoHit(Entity ent, ref ThrowDoHitEvent args)
+ {
+ if (!_handsQuery.TryGetComponent(args.Target, out var handsComp))
+ return; // don't do anything for walls etc
+
+ // Is the catcher in combat mode if required?
+ if (ent.Comp.RequireCombatMode && (!_combatModeQuery.TryComp(args.Target, out var combatModeComp) || !combatModeComp.IsInCombatMode))
+ return;
+
+ // Is the catcher able to catch this item?
+ if (!_whitelist.IsWhitelistPassOrNull(ent.Comp.CatcherWhitelist, args.Target))
+ return;
+
+ var attemptEv = new CatchAttemptEvent(ent.Owner, ent.Comp.CatchChance);
+ RaiseLocalEvent(args.Target, ref attemptEv);
+
+ if (attemptEv.Cancelled)
+ return;
+
+ // TODO: Replace with RandomPredicted once the engine PR is merged
+ var seed = HashCode.Combine((int)_timing.CurTick.Value, GetNetEntity(ent).Id);
+ var rand = new System.Random(seed);
+ if (!rand.Prob(ent.Comp.CatchChance))
+ return;
+
+ // Try to catch!
+ if (!_hands.TryPickupAnyHand(args.Target, ent.Owner, handsComp: handsComp, animate: false))
+ return; // The hands are full!
+
+ // Success!
+
+ // We picked it up already but we still have to raise the throwing stop (but not the landing) events at the right time,
+ // otherwise it will raise the events for that later while still in your hand
+ _thrown.StopThrow(ent.Owner, args.Component);
+
+ // Collisions don't work properly with PopupPredicted or PlayPredicted.
+ // So we make this server only.
+ if (_net.IsClient)
+ return;
+
+ var selfMessage = Loc.GetString("catchable-component-success-self", ("item", ent.Owner), ("catcher", Identity.Entity(args.Target, EntityManager)));
+ var othersMessage = Loc.GetString("catchable-component-success-others", ("item", ent.Owner), ("catcher", Identity.Entity(args.Target, EntityManager)));
+ _popup.PopupEntity(selfMessage, args.Target, args.Target);
+ _popup.PopupEntity(othersMessage, args.Target, Filter.PvsExcept(args.Target), true);
+ _audio.PlayPvs(ent.Comp.CatchSuccessSound, args.Target);
+ }
+}
diff --git a/Resources/Locale/en-US/bonk/components/bonkable-component.ftl b/Resources/Locale/en-US/bonk/components/bonkable-component.ftl
deleted file mode 100644
index 1a79da3509..0000000000
--- a/Resources/Locale/en-US/bonk/components/bonkable-component.ftl
+++ /dev/null
@@ -1,4 +0,0 @@
-forced-bonkable-success-message = { CAPITALIZE($bonker) } bonks {$victim}s head against { THE($bonkable) }!
-
-bonkable-success-message-user = You bonk your head against { THE($bonkable) }!
-bonkable-success-message-others = {$victim} bonks their head against { THE($bonkable) }!
diff --git a/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl b/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl
index 52dbf9010e..96ebaa3ed0 100644
--- a/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl
+++ b/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl
@@ -10,7 +10,6 @@ hypospray-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/
hypospray-component-inject-other-message = You inject {$other}.
hypospray-component-inject-self-message = You inject yourself.
-hypospray-component-inject-self-clumsy-message = Oops! You injected yourself.
hypospray-component-empty-message = Nothing to inject.
hypospray-component-feel-prick-message = You feel a tiny prick!
hypospray-component-transfer-already-full-message = {$owner} is already full!
diff --git a/Resources/Locale/en-US/clown/components/clumsy-component.ftl b/Resources/Locale/en-US/clown/components/clumsy-component.ftl
new file mode 100644
index 0000000000..055b3b9c4e
--- /dev/null
+++ b/Resources/Locale/en-US/clown/components/clumsy-component.ftl
@@ -0,0 +1,10 @@
+clumsy-vaulting-fail-forced-message = { CAPITALIZE($bonker) } bonks { $victim }s head against { THE($bonkable) }!
+clumsy-vaulting-fail-message-user = You bonk your head against { THE($bonkable) }!
+clumsy-vaulting-fail-message-others = { $victim } bonks their head against { THE($bonkable) }!
+
+clumsy-gun-fail-message = The gun blows up in your face!
+
+clumsy-hypospray-fail-message = Oops! You injected yourself.
+
+clumsy-catch-fail-message-user = { CAPITALIZE(THE($item)) } hits your head!
+clumsy-catch-fail-message-others = { CAPITALIZE(THE($item)) } hits { THE($catcher) }'s head!
diff --git a/Resources/Locale/en-US/throwing/catchable.ftl b/Resources/Locale/en-US/throwing/catchable.ftl
new file mode 100644
index 0000000000..8db3befd68
--- /dev/null
+++ b/Resources/Locale/en-US/throwing/catchable.ftl
@@ -0,0 +1,4 @@
+catchable-component-success-self = You catch {THE($item)}!
+catchable-component-success-others = {CAPITALIZE(THE($catcher))} catches {THE($item)}!
+catchable-component-fail-self = You fail to catch {THE($item)}!
+catchable-component-fail-others = {CAPITALIZE(THE($catcher))} fails to catch {THE($item)}!
diff --git a/Resources/Locale/en-US/weapons/ranged/gun.ftl b/Resources/Locale/en-US/weapons/ranged/gun.ftl
index 18e01e31c8..a364075be9 100644
--- a/Resources/Locale/en-US/weapons/ranged/gun.ftl
+++ b/Resources/Locale/en-US/weapons/ranged/gun.ftl
@@ -4,7 +4,6 @@ gun-fire-rate-examine = Fire rate is [color={$color}]{$fireRate}[/color] per sec
gun-selector-verb = Change to {$mode}
gun-selected-mode = Selected {$mode}
gun-disabled = You can't use guns!
-gun-clumsy = The gun blows up in your face!
gun-set-fire-mode = Set to {$mode}
gun-magazine-whitelist-fail = That won't fit into the gun!
gun-magazine-fired-empty = No ammo left!
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index 967dda00db..2a599d70be 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -1464,6 +1464,9 @@
Piercing: 4
groups:
Burn: 3
+ catchingFailDamage:
+ types:
+ Blunt: 1
clumsySound:
path: /Audio/Animals/monkey_scream.ogg
@@ -1643,6 +1646,9 @@
Piercing: 7
groups:
Burn: 3
+ catchingFailDamage:
+ types:
+ Blunt: 1
clumsySound:
path: /Audio/Voice/Reptilian/reptilian_scream.ogg
diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
index 0aee45e28e..1b81b0d099 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
@@ -239,6 +239,9 @@
Piercing: 4
groups:
Burn: 3
+ catchingFailDamage:
+ types:
+ Blunt: 1
- type: MeleeWeapon
angle: 30
animation: WeaponArcFist
diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml
index 4daffe1c56..e9279193b7 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml
@@ -396,6 +396,20 @@
- type: Sprite
sprite: Objects/Fun/Balls/basketball.rsi
state: icon
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape: !type:PhysShapeCircle
+ radius: 0.25
+ density: 20
+ mask:
+ - ItemMask
+ restitution: 0.8 # bouncy
+ friction: 0.2
+ - type: Catchable
+ catchChance: 0.8
+ catchSuccessSound:
+ path: /Audio/Effects/Footsteps/bounce.ogg
- type: EmitSoundOnCollide
sound:
path: /Audio/Effects/Footsteps/bounce.ogg
@@ -414,6 +428,23 @@
- type: Sprite
sprite: Objects/Fun/Balls/football.rsi
state: icon
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape: !type:PhysShapeCircle
+ radius: 0.25
+ density: 20
+ mask:
+ - ItemMask
+ restitution: 0.5 # a little bouncy
+ friction: 0.2
+ - type: Catchable
+ catchChance: 0.8
+ catchSuccessSound:
+ path: /Audio/Effects/Footsteps/bounce.ogg
+ - type: EmitSoundOnCollide
+ sound:
+ path: /Audio/Effects/Footsteps/bounce.ogg
- type: Item
size: Small
sprite: Objects/Fun/Balls/football.rsi
@@ -427,6 +458,21 @@
- type: Sprite
sprite: Objects/Fun/Balls/beach_ball.rsi
state: icon
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape: !type:PhysShapeCircle
+ radius: 0.3
+ position: "0,-0.2"
+ density: 20
+ mask:
+ - ItemMask
+ restitution: 0.1 # not bouncy
+ friction: 0.2
+ - type: Catchable
+ catchChance: 0.8
+ catchSuccessSound:
+ path: /Audio/Effects/Footsteps/bounce.ogg
- type: EmitSoundOnCollide
sound:
path: /Audio/Effects/Footsteps/bounce.ogg
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml
index 60821cfe76..c64d0550ce 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml
@@ -19,6 +19,9 @@
Piercing: 4
groups:
Burn: 3
+ catchingFailDamage:
+ types:
+ Blunt: 1
- type: SleepEmitSound
snore: /Audio/Voice/Misc/silly_snore.ogg
interval: 10