Catchable items, playable basketball (#37702)

* catching

* fix

* improve

* fix linter

* cleanup

* fix prediction

* do the same here

* fix comment
This commit is contained in:
slarticodefast
2025-07-06 18:54:20 +02:00
committed by GitHub
parent cb21b72600
commit 22e3d533d3
16 changed files with 304 additions and 24 deletions

View File

@@ -14,6 +14,7 @@ using Content.Shared.Forensics.Components;
using Content.Shared.HealthExaminable; using Content.Shared.HealthExaminable;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Rejuvenate; using Content.Shared.Rejuvenate;
using Content.Shared.Speech.EntitySystems; using Content.Shared.Speech.EntitySystems;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
@@ -222,7 +223,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
// TODO: Replace with RandomPredicted once the engine PR is merged // 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 // 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 rand = new System.Random(seed);
var prob = Math.Clamp(totalFloat / 25, 0, 1); var prob = Math.Clamp(totalFloat / 25, 0, 1);
if (totalFloat > 0 && rand.Prob(prob)) if (totalFloat > 0 && rand.Prob(prob))

View File

@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Clumsy; namespace Content.Shared.Clumsy;
/// <summary> /// <summary>
/// A simple clumsy tag-component. /// Makes the entity clumsy, randomly failing some interactions and hurting themselves.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ClumsyComponent : Component public sealed partial class ClumsyComponent : Component
@@ -48,11 +48,17 @@ public sealed partial class ClumsyComponent : Component
public TimeSpan GunShootFailStunTime = TimeSpan.FromSeconds(3); public TimeSpan GunShootFailStunTime = TimeSpan.FromSeconds(3);
/// <summary> /// <summary>
/// Stun time after failing to shoot a gun. /// Damage taken after failing to shoot a gun.
/// </summary> /// </summary>
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public DamageSpecifier? GunShootFailDamage; public DamageSpecifier? GunShootFailDamage;
/// <summary>
/// Damage taken after failing to catch an item.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier? CatchingFailDamage;
/// <summary> /// <summary>
/// Noise to play after failing to shoot a gun. Boom! /// Noise to play after failing to shoot a gun. Boom!
/// </summary> /// </summary>
@@ -77,6 +83,12 @@ public sealed partial class ClumsyComponent : Component
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public bool ClumsyGuns = true; public bool ClumsyGuns = true;
/// <summary>
/// Whether or not to apply Clumsy to catching items.
/// </summary>
[DataField, AutoNetworkedField]
public bool ClumsyCatching = true;
/// <summary> /// <summary>
/// Whether or not to apply Clumsy to vaulting. /// Whether or not to apply Clumsy to vaulting.
/// </summary> /// </summary>
@@ -87,17 +99,23 @@ public sealed partial class ClumsyComponent : Component
/// Lets you define a new "failed" message for each event. /// Lets you define a new "failed" message for each event.
/// </summary> /// </summary>
[DataField] [DataField]
public LocId HypoFailedMessage = "hypospray-component-inject-self-clumsy-message"; public LocId HypoFailedMessage = "clumsy-hypospray-fail-message";
[DataField] [DataField]
public LocId GunFailedMessage = "gun-clumsy"; public LocId GunFailedMessage = "clumsy-gun-fail-message";
[DataField] [DataField]
public LocId VaulingFailedMessageSelf = "bonkable-success-message-user"; public LocId CatchingFailedMessageSelf = "clumsy-catch-fail-message-user";
[DataField] [DataField]
public LocId VaulingFailedMessageOthers = "bonkable-success-message-others"; public LocId CatchingFailedMessageOthers = "clumsy-catch-fail-message-others";
[DataField] [DataField]
public LocId VaulingFailedMessageForced = "forced-bonkable-success-message"; public LocId VaulingFailedMessageSelf = "clumsy-vaulting-fail-message-user";
[DataField]
public LocId VaulingFailedMessageOthers = "clumsy-vaulting-fail-message-others";
[DataField]
public LocId VaulingFailedMessageForced = "clumsy-vaulting-fail-forced-message";
} }

View File

@@ -6,10 +6,14 @@ using Content.Shared.Damage;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Medical; using Content.Shared.Medical;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -17,19 +21,20 @@ namespace Content.Shared.Clumsy;
public sealed class ClumsySystem : EntitySystem public sealed class ClumsySystem : EntitySystem
{ {
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly SharedStunSystem _stun = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly INetManager _net = default!;
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent); SubscribeLocalEvent<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent); SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent); SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent);
SubscribeLocalEvent<ClumsyComponent, CatchAttemptEvent>(OnCatchAttempt);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeClimbEvent>(OnBeforeClimbEvent); SubscribeLocalEvent<ClumsyComponent, SelfBeforeClimbEvent>(OnBeforeClimbEvent);
} }
@@ -43,12 +48,15 @@ public sealed class ClumsySystem : EntitySystem
if (!ent.Comp.ClumsyHypo) if (!ent.Comp.ClumsyHypo)
return; 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; return;
args.TargetGettingInjected = args.EntityUsingHypospray; args.TargetGettingInjected = args.EntityUsingHypospray;
args.InjectMessageOverride = "hypospray-component-inject-self-clumsy-message"; args.InjectMessageOverride = Loc.GetString(ent.Comp.HypoFailedMessage);
_audio.PlayPvs(ent.Comp.ClumsySound, ent); _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingHypospray);
} }
private void BeforeDefibrillatorZapsEvent(Entity<ClumsyComponent> ent, ref SelfBeforeDefibrillatorZapsEvent args) private void BeforeDefibrillatorZapsEvent(Entity<ClumsyComponent> ent, ref SelfBeforeDefibrillatorZapsEvent args)
@@ -59,7 +67,10 @@ public sealed class ClumsySystem : EntitySystem
if (!ent.Comp.ClumsyDefib) if (!ent.Comp.ClumsyDefib)
return; 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; return;
args.DefibTarget = args.EntityUsingDefib; 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) private void BeforeGunShotEvent(Entity<ClumsyComponent> ent, ref SelfBeforeGunShotEvent args)
{ {
// Clumsy people sometimes can't shoot :( // Clumsy people sometimes can't shoot :(
@@ -78,7 +120,10 @@ public sealed class ClumsySystem : EntitySystem
if (args.Gun.Comp.ClumsyProof) if (args.Gun.Comp.ClumsyProof)
return; 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; return;
if (ent.Comp.GunShootFailDamage != null) if (ent.Comp.GunShootFailDamage != null)
@@ -90,7 +135,7 @@ public sealed class ClumsySystem : EntitySystem
_audio.PlayPvs(ent.Comp.GunShootFailSound, ent); _audio.PlayPvs(ent.Comp.GunShootFailSound, ent);
_audio.PlayPvs(ent.Comp.ClumsySound, 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(); args.Cancel();
} }
@@ -100,9 +145,9 @@ public sealed class ClumsySystem : EntitySystem
if (!ent.Comp.ClumsyVaulting) if (!ent.Comp.ClumsyVaulting)
return; return;
// This event is called in shared, thats why it has all the extra prediction stuff. // TODO: Replace with RandomPredicted once the engine PR is merged
var rand = new System.Random((int)_timing.CurTick.Value); 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)) if (!_cfg.GetCVar(CCVars.GameTableBonk) && !rand.Prob(ent.Comp.ClumsyDefaultCheck))
return; return;

View File

@@ -184,5 +184,25 @@ namespace Content.Shared.Random.Helpers
// Shouldn't happen // Shouldn't happen
throw new InvalidOperationException($"Invalid weighted pick for {prototype.ID}!"); 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;
}
} }
} }

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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) }!

View 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-other-message = You inject {$other}.
hypospray-component-inject-self-message = You inject yourself. 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-empty-message = Nothing to inject.
hypospray-component-feel-prick-message = You feel a tiny prick! hypospray-component-feel-prick-message = You feel a tiny prick!
hypospray-component-transfer-already-full-message = {$owner} is already full! hypospray-component-transfer-already-full-message = {$owner} is already full!

View File

@@ -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!

View File

@@ -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)}!

View 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-selector-verb = Change to {$mode}
gun-selected-mode = Selected {$mode} gun-selected-mode = Selected {$mode}
gun-disabled = You can't use guns! gun-disabled = You can't use guns!
gun-clumsy = The gun blows up in your face!
gun-set-fire-mode = Set to {$mode} gun-set-fire-mode = Set to {$mode}
gun-magazine-whitelist-fail = That won't fit into the gun! gun-magazine-whitelist-fail = That won't fit into the gun!
gun-magazine-fired-empty = No ammo left! gun-magazine-fired-empty = No ammo left!

View File

@@ -1464,6 +1464,9 @@
Piercing: 4 Piercing: 4
groups: groups:
Burn: 3 Burn: 3
catchingFailDamage:
types:
Blunt: 1
clumsySound: clumsySound:
path: /Audio/Animals/monkey_scream.ogg path: /Audio/Animals/monkey_scream.ogg
@@ -1643,6 +1646,9 @@
Piercing: 7 Piercing: 7
groups: groups:
Burn: 3 Burn: 3
catchingFailDamage:
types:
Blunt: 1
clumsySound: clumsySound:
path: /Audio/Voice/Reptilian/reptilian_scream.ogg path: /Audio/Voice/Reptilian/reptilian_scream.ogg

View File

@@ -239,6 +239,9 @@
Piercing: 4 Piercing: 4
groups: groups:
Burn: 3 Burn: 3
catchingFailDamage:
types:
Blunt: 1
- type: MeleeWeapon - type: MeleeWeapon
angle: 30 angle: 30
animation: WeaponArcFist animation: WeaponArcFist

View File

@@ -396,6 +396,20 @@
- type: Sprite - type: Sprite
sprite: Objects/Fun/Balls/basketball.rsi sprite: Objects/Fun/Balls/basketball.rsi
state: icon 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 - type: EmitSoundOnCollide
sound: sound:
path: /Audio/Effects/Footsteps/bounce.ogg path: /Audio/Effects/Footsteps/bounce.ogg
@@ -414,6 +428,23 @@
- type: Sprite - type: Sprite
sprite: Objects/Fun/Balls/football.rsi sprite: Objects/Fun/Balls/football.rsi
state: icon 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 - type: Item
size: Small size: Small
sprite: Objects/Fun/Balls/football.rsi sprite: Objects/Fun/Balls/football.rsi
@@ -427,6 +458,21 @@
- type: Sprite - type: Sprite
sprite: Objects/Fun/Balls/beach_ball.rsi sprite: Objects/Fun/Balls/beach_ball.rsi
state: icon 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 - type: EmitSoundOnCollide
sound: sound:
path: /Audio/Effects/Footsteps/bounce.ogg path: /Audio/Effects/Footsteps/bounce.ogg

View File

@@ -19,6 +19,9 @@
Piercing: 4 Piercing: 4
groups: groups:
Burn: 3 Burn: 3
catchingFailDamage:
types:
Blunt: 1
- type: SleepEmitSound - type: SleepEmitSound
snore: /Audio/Voice/Misc/silly_snore.ogg snore: /Audio/Voice/Misc/silly_snore.ogg
interval: 10 interval: 10