Catchable items, playable basketball (#37702)
* catching * fix * improve * fix linter * cleanup * fix prediction * do the same here * fix comment
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
Content.Shared/Throwing/CatchAttemptEvent.cs
Normal file
7
Content.Shared/Throwing/CatchAttemptEvent.cs
Normal 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);
|
||||||
39
Content.Shared/Throwing/CatchableComponent.cs
Normal file
39
Content.Shared/Throwing/CatchableComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
84
Content.Shared/Throwing/CatchableSystem.cs
Normal file
84
Content.Shared/Throwing/CatchableSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }!
|
|
||||||
@@ -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!
|
||||||
|
|||||||
10
Resources/Locale/en-US/clown/components/clumsy-component.ftl
Normal file
10
Resources/Locale/en-US/clown/components/clumsy-component.ftl
Normal 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!
|
||||||
4
Resources/Locale/en-US/throwing/catchable.ftl
Normal file
4
Resources/Locale/en-US/throwing/catchable.ftl
Normal 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)}!
|
||||||
@@ -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!
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user