]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Catchable items, playable basketball (#37702)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Sun, 6 Jul 2025 16:54:20 +0000 (18:54 +0200)
committerGitHub <noreply@github.com>
Sun, 6 Jul 2025 16:54:20 +0000 (09:54 -0700)
* catching

* fix

* improve

* fix linter

* cleanup

* fix prediction

* do the same here

* fix comment

16 files changed:
Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
Content.Shared/Clumsy/ClumsyComponent.cs
Content.Shared/Clumsy/ClumsySystem.cs
Content.Shared/Random/Helpers/SharedRandomExtensions.cs
Content.Shared/Throwing/CatchAttemptEvent.cs [new file with mode: 0644]
Content.Shared/Throwing/CatchableComponent.cs [new file with mode: 0644]
Content.Shared/Throwing/CatchableSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/bonk/components/bonkable-component.ftl [deleted file]
Resources/Locale/en-US/chemistry/components/hypospray-component.ftl
Resources/Locale/en-US/clown/components/clumsy-component.ftl [new file with mode: 0644]
Resources/Locale/en-US/throwing/catchable.ftl [new file with mode: 0644]
Resources/Locale/en-US/weapons/ranged/gun.ftl
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/Entities/Mobs/Player/guardian.yml
Resources/Prototypes/Entities/Objects/Fun/toys.yml
Resources/Prototypes/Roles/Jobs/Civilian/clown.yml

index b5d510018996c8cba91a4ea0ccd528edf414d6d6..ac385040a9415f9e37fea6a2c30ecb25c1c5c063 100644 (file)
@@ -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))
index 6b013a5c2f14900e659db5b02736151ad69aa152..9e6e851c8c1284fe51483f1ae496afacc90a711b 100644 (file)
@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
 namespace Content.Shared.Clumsy;
 
 /// <summary>
-/// A simple clumsy tag-component.
+/// Makes the entity clumsy, randomly failing some interactions and hurting themselves.
 /// </summary>
 [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);
 
     /// <summary>
-    ///     Stun time after failing to shoot a gun.
+    ///     Damage taken after failing to shoot a gun.
     /// </summary>
     [DataField, AutoNetworkedField]
     public DamageSpecifier? GunShootFailDamage;
 
+    /// <summary>
+    ///     Damage taken after failing to catch an item.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public DamageSpecifier? CatchingFailDamage;
+
     /// <summary>
     ///     Noise to play after failing to shoot a gun. Boom!
     /// </summary>
@@ -77,6 +83,12 @@ public sealed partial class ClumsyComponent : Component
     [DataField, AutoNetworkedField]
     public bool ClumsyGuns = true;
 
+    /// <summary>
+    ///      Whether or not to apply Clumsy to catching items.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool ClumsyCatching = true;
+
     /// <summary>
     ///      Whether or not to apply Clumsy to vaulting.
     /// </summary>
@@ -87,17 +99,23 @@ public sealed partial class ClumsyComponent : Component
     ///      Lets you define a new "failed" message for each event.
     /// </summary>
     [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";
 }
index 348d99182a668e1c6b47f516ff1b89fc7a802cc4..9e0e82364f88598733cff6681dabd5807e182db6 100644 (file)
@@ -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<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent);
         SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent);
         SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent);
+        SubscribeLocalEvent<ClumsyComponent, CatchAttemptEvent>(OnCatchAttempt);
         SubscribeLocalEvent<ClumsyComponent, SelfBeforeClimbEvent>(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<ClumsyComponent> 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<ClumsyComponent> 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<ClumsyComponent> 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;
 
index 42d92a90653b79062570899d1d62bae8da944851..87e839b56f400d7826e9e88c3d4ad46365213f46 100644 (file)
@@ -184,5 +184,25 @@ namespace Content.Shared.Random.Helpers
             // Shouldn't happen
             throw new InvalidOperationException($"Invalid weighted pick for {prototype.ID}!");
         }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <example>
+        /// 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.
+        /// <code>
+        /// var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(ent).Id });
+        /// </code>
+        /// </example>
+        public static int HashCodeCombine(List<int> 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 (file)
index 0000000..03206bd
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Content.Shared.Throwing;
+
+/// <summary>
+/// Raised on someone when they try to catch an item.
+/// </summary>
+[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 (file)
index 0000000..ce68374
--- /dev/null
@@ -0,0 +1,39 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Throwing;
+
+/// <summary>
+/// Allows this entity to be caught in your hands when someone else throws it at you.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class CatchableComponent : Component
+{
+    /// <summary>
+    /// If true this item can only be caught while in combat mode.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool RequireCombatMode;
+
+    /// <summary>
+    /// The chance of successfully catching.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float CatchChance = 1.0f;
+
+    /// <summary>
+    /// Optional whitelist for who can catch this item.
+    /// </summary>
+    /// <summary>
+    /// Example usecase: Only someone who knows martial arts can catch grenades.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityWhitelist? CatcherWhitelist;
+
+    /// <summary>
+    /// The sound to play when successfully catching.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? CatchSuccessSound;
+}
diff --git a/Content.Shared/Throwing/CatchableSystem.cs b/Content.Shared/Throwing/CatchableSystem.cs
new file mode 100644 (file)
index 0000000..8f2fd35
--- /dev/null
@@ -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<HandsComponent> _handsQuery;
+    private EntityQuery<CombatModeComponent> _combatModeQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<CatchableComponent, ThrowDoHitEvent>(OnDoHit);
+
+        _handsQuery = GetEntityQuery<HandsComponent>();
+        _combatModeQuery = GetEntityQuery<CombatModeComponent>();
+    }
+
+    private void OnDoHit(Entity<CatchableComponent> 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 (file)
index 1a79da3..0000000
+++ /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) }!
index 52dbf9010e3b3478259a0588fb3ec6107e29ee97..96ebaa3ed016ab0ff013339edd0b82784aa30883 100644 (file)
@@ -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 (file)
index 0000000..055b3b9
--- /dev/null
@@ -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 (file)
index 0000000..8db3bef
--- /dev/null
@@ -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)}!
index 18e01e31c83d383f1344f03d0e4e8f135f55321d..a364075be951c2264405f2b988cb9e7735e85d2c 100644 (file)
@@ -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!
index 967dda00db04811a10242ca66ad6e1fa5efed344..2a599d70bea8ab95ff06b41293eb8817071539a5 100644 (file)
         Piercing: 4
       groups:
         Burn: 3
+    catchingFailDamage:
+      types:
+        Blunt: 1
     clumsySound:
       path: /Audio/Animals/monkey_scream.ogg
 
         Piercing: 7
       groups:
         Burn: 3
+    catchingFailDamage:
+      types:
+        Blunt: 1
     clumsySound:
       path: /Audio/Voice/Reptilian/reptilian_scream.ogg
 
index 0aee45e28edb069275ea3e942cf71b8e84d93c8f..1b81b0d0995dac9846e211d4f6c7a068300b74a4 100644 (file)
           Piercing: 4
         groups:
           Burn: 3
+      catchingFailDamage:
+        types:
+          Blunt: 1
     - type: MeleeWeapon
       angle: 30
       animation: WeaponArcFist
index 4daffe1c560a855f5d323c1402a009cc4cea46a2..e9279193b7ebe9e93e67684c3b3a1f720377afa6 100644 (file)
   - 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
   - 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
   - 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
index 60821cfe76fe85795baef0ba4f8fe1e437f30a4d..c64d0550ce1813467cd24ad86cb033ea89d9c237 100644 (file)
@@ -19,6 +19,9 @@
           Piercing: 4
         groups:
           Burn: 3
+      catchingFailDamage:
+        types:
+          Blunt: 1
     - type: SleepEmitSound
       snore: /Audio/Voice/Misc/silly_snore.ogg
       interval: 10