--- /dev/null
+using Content.Shared.Traits.Assorted;
+using Content.Client.Camera;
+using Robust.Shared.Random;
+using Robust.Client.GameObjects;
+using Robust.Client.Player;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Traits;
+
+public sealed class ParacusiaSystem : SharedParacusiaSystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly CameraRecoilSystem _camera = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<ParacusiaComponent, ComponentStartup>(OnComponentStartup);
+ SubscribeLocalEvent<ParacusiaComponent, PlayerDetachedEvent>(OnPlayerDetach);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ if (_player.LocalPlayer?.ControlledEntity is not EntityUid localPlayer)
+ return;
+
+ PlayParacusiaSounds(localPlayer);
+ }
+
+ private void OnComponentStartup(EntityUid uid, ParacusiaComponent component, ComponentStartup args)
+ {
+ component.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.MinTimeBetweenIncidents, component.MaxTimeBetweenIncidents));
+ }
+
+ private void OnPlayerDetach(EntityUid uid, ParacusiaComponent component, PlayerDetachedEvent args)
+ {
+ component.Stream?.Stop();
+ }
+
+ private void PlayParacusiaSounds(EntityUid uid)
+ {
+ if (!TryComp<ParacusiaComponent>(uid, out var paracusia))
+ return;
+
+ if (_timing.CurTime <= paracusia.NextIncidentTime)
+ return;
+
+ // Set the new time.
+ var timeInterval = _random.NextFloat(paracusia.MinTimeBetweenIncidents, paracusia.MaxTimeBetweenIncidents);
+ paracusia.NextIncidentTime += TimeSpan.FromSeconds(timeInterval);
+
+ // Offset position where the sound is played
+ var randomOffset =
+ new Vector2
+ (
+ _random.NextFloat(-paracusia.MaxSoundDistance, paracusia.MaxSoundDistance),
+ _random.NextFloat(-paracusia.MaxSoundDistance, paracusia.MaxSoundDistance)
+ );
+
+ var newCoords = Transform(uid).Coordinates.Offset(randomOffset);
+
+ // Play the sound
+ paracusia.Stream = _audio.PlayStatic(paracusia.Sounds, uid, newCoords);
+ }
+
+}
/// BoozePower is how long each metabolism cycle will make the drunk effect last for.
/// </summary>
[DataField("boozePower")]
- public float BoozePower = 2f;
+ public float BoozePower = 3f;
/// <summary>
/// Whether speech should be slurred.
--- /dev/null
+using Content.Shared.Traits.Assorted;
+
+namespace Content.Server.Traits.Assorted;
+
+public sealed class ParacusiaSystem : SharedParacusiaSystem
+{
+
+}
using Content.Shared.Speech.EntitySystems;
using Content.Shared.StatusEffect;
+using Content.Shared.Traits.Assorted;
namespace Content.Shared.Drunk;
if (applySlur)
_slurredSystem.DoSlur(uid, TimeSpan.FromSeconds(boozePower), status);
+ if (TryComp<LightweightDrunkComponent>(uid, out var trait))
+ boozePower *= trait.BoozeStrengthMultiplier;
+
if (!_statusEffectsSystem.HasStatusEffect(uid, DrunkKey, status))
{
_statusEffectsSystem.TryAddStatusEffect<DrunkComponent>(uid, DrunkKey, TimeSpan.FromSeconds(boozePower), true, status);
--- /dev/null
+using Robust.Shared.GameStates;
+using Content.Shared.Drunk;
+
+namespace Content.Shared.Traits.Assorted;
+
+/// <summary>
+/// Used for the lightweight trait. DrunkSystem will check for this component and modify the boozePower accordingly if it finds it.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedDrunkSystem))]
+public sealed class LightweightDrunkComponent : Component
+{
+ [DataField("boozeStrengthMultiplier"), ViewVariables(VVAccess.ReadWrite)]
+ public float BoozeStrengthMultiplier = 4f;
+}
--- /dev/null
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using System;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Traits.Assorted;
+
+/// <summary>
+/// This component is used for paracusia, which causes auditory hallucinations.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedParacusiaSystem))]
+public sealed class ParacusiaComponent : Component
+{
+ /// <summary>
+ /// The maximum time between incidents in seconds
+ /// </summary>
+ [DataField("maxTimeBetweenIncidents", required: true), ViewVariables(VVAccess.ReadWrite)]
+ public float MaxTimeBetweenIncidents = 30f;
+
+ /// <summary>
+ /// The minimum time between incidents in seconds
+ /// </summary>
+ [DataField("minTimeBetweenIncidents", required: true), ViewVariables(VVAccess.ReadWrite)]
+ public float MinTimeBetweenIncidents = 60f;
+
+ /// <summary>
+ /// How far away at most can the sound be?
+ /// </summary>
+ [DataField("maxSoundDistance", required: true), ViewVariables(VVAccess.ReadWrite)]
+ public float MaxSoundDistance;
+
+ /// <summary>
+ /// The sounds to choose from
+ /// </summary>
+ [DataField("sounds", required: true)]
+ public SoundSpecifier Sounds = default!;
+
+ [DataField("timeBetweenIncidents", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan NextIncidentTime;
+
+ public IPlayingAudioStream? Stream;
+}
+
+[Serializable, NetSerializable]
+public sealed class ParacusiaComponentState : ComponentState
+{
+ public readonly float MaxTimeBetweenIncidents;
+ public readonly float MinTimeBetweenIncidents;
+ public readonly float MaxSoundDistance;
+ public readonly SoundSpecifier Sounds = default!;
+
+ public ParacusiaComponentState(float maxTimeBetweenIncidents, float minTimeBetweenIncidents, float maxSoundDistance, SoundSpecifier sounds)
+ {
+ MaxTimeBetweenIncidents = maxTimeBetweenIncidents;
+ MinTimeBetweenIncidents = minTimeBetweenIncidents;
+ MaxSoundDistance = maxSoundDistance;
+ Sounds = sounds;
+ }
+}
--- /dev/null
+using Content.Shared.GameTicking;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Traits.Assorted;
+
+public abstract class SharedParacusiaSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<ParacusiaComponent, ComponentGetState>(GetCompState);
+ SubscribeLocalEvent<ParacusiaComponent, ComponentHandleState>(HandleCompState);
+ }
+
+ private void GetCompState(EntityUid uid, ParacusiaComponent component, ref ComponentGetState args)
+ {
+ args.State = new ParacusiaComponentState(component.MaxTimeBetweenIncidents, component.MinTimeBetweenIncidents, component.MaxSoundDistance, component.Sounds);
+ }
+
+ private void HandleCompState(EntityUid uid, ParacusiaComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not ParacusiaComponentState state)
+ return;
+
+ component.MaxTimeBetweenIncidents = state.MaxTimeBetweenIncidents;
+ component.MinTimeBetweenIncidents = state.MinTimeBetweenIncidents;
+ component.MaxSoundDistance = state.MaxSoundDistance;
+ component.Sounds = state.Sounds;
+ }
+}
trait-sneezing-desc = You sneeze and cough uncontrollably
permanent-blindness-trait-examined = [color=lightblue]{CAPITALIZE(POSS-ADJ($target))} eyes are glassy and unfocused. It doesn't seem like {SUBJECT($target)} can see you.[/color]
+
+trait-lightweight-name = Lightweight Drunk
+trait-lightweight-desc = Alcohol has a stronger effect on you
+
+trait-muted-name = Muted
+trait-muted-desc = You can't speak
+
+trait-paracusia-name = Paracusia
+trait-paracusia-desc = You hear sounds that aren't really there
--- /dev/null
+- type: soundCollection
+ id: Paracusia
+ files:
+ #- /Audio/Effects/adminhelp.ogg
+ #- /Audio/Machines/Nuke/nuke_alarm.ogg
+ #- /Audio/Misc/emergency_meeting.ogg
+ - /Audio/Effects/countdown.ogg
+ - /Audio/Effects/explosion1.ogg
+ - /Audio/Effects/explosion2.ogg
+ - /Audio/Effects/explosion3.ogg
+ - /Audio/Effects/explosion4.ogg
+ - /Audio/Effects/explosion5.ogg
+ - /Audio/Effects/explosion6.ogg
+ - /Audio/Effects/glass_break1.ogg
+ - /Audio/Effects/glass_break2.ogg
+ - /Audio/Effects/glass_break3.ogg
+ - /Audio/Effects/bodyfall1.ogg
+ - /Audio/Effects/bodyfall2.ogg
+ - /Audio/Effects/bodyfall3.ogg
+ - /Audio/Effects/bodyfall4.ogg
+ - /Audio/Effects/demon_dies.ogg
+ - /Audio/Effects/demon_attack1.ogg
+ - /Audio/Effects/bang.ogg
+ - /Audio/Effects/clang.ogg
+ - /Audio/Effects/metalbreak.ogg
+ - /Audio/Effects/minibombcountdown.ogg
+ - /Audio/Effects/sadtrombone.ogg
+ - /Audio/Effects/sparks1.ogg
+ - /Audio/Effects/sparks2.ogg
+ - /Audio/Effects/sparks3.ogg
+ - /Audio/Effects/sparks4.ogg
+ - /Audio/Effects/radpulse1.ogg
+ - /Audio/Effects/radpulse5.ogg
+ - /Audio/Effects/radpulse9.ogg
+ - /Audio/Effects/Chemistry/bubbles.ogg
+ - /Audio/Machines/airlock_close.ogg
+ - /Audio/Machines/airlock_deny.ogg
+ - /Audio/Machines/airlock_open.ogg
+ - /Audio/Machines/airlock_ext_open.ogg
+ - /Audio/Machines/anomaly_generate.ogg
+ - /Audio/Machines/phasein.ogg
+ - /Audio/Machines/vending_restock_start.ogg
+ - /Audio/Machines/vending_restock_done.ogg
+ - /Audio/Magic/disintegrate.ogg
+ - /Audio/Magic/staff_animation.ogg
+ - /Audio/Weapons/ebladeon.ogg
+ - /Audio/Weapons/smash.ogg
+ - /Audio/Weapons/bladeslice.ogg
+ - /Audio/Weapons/punch1.ogg
+ - /Audio/Weapons/punch2.ogg
+ - /Audio/Weapons/punch3.ogg
+ - /Audio/Weapons/punch4.ogg
+ - /Audio/Weapons/genhit1.ogg
+ - /Audio/Weapons/Guns/Hits/bullet_hit.ogg
+ - /Audio/Weapons/Guns/Hits/snap.ogg
+ - /Audio/Weapons/Guns/Gunshots/atreides.ogg
+ - /Audio/Weapons/Guns/Gunshots/c-20r.ogg
+ - /Audio/Weapons/Guns/Gunshots/pistol.ogg
+ - /Audio/Items/bikehorn.ogg
+ - /Audio/Items/Toys/weh.ogg
+ - /Audio/Items/Toys/toysqueak1.ogg
+ - /Audio/Items/Toys/toysqueak2.ogg
+ - /Audio/Items/Toys/toysqueak3.ogg
+ - /Audio/Voice/Talk/lizard.ogg
+ - /Audio/Voice/Talk/pai.ogg
+ - /Audio/Voice/Talk/speak_1.ogg
+ - /Audio/Voice/Talk/speak_2_ask.ogg
+ - /Audio/Voice/Talk/speak_3_exclaim.ogg
+ - /Audio/Voice/Human/malescream_1.ogg
+ - /Audio/Voice/Human/malescream_6.ogg
+ - /Audio/Voice/Human/femalescream_2.ogg
+ - /Audio/Voice/Human/femalescream_4.ogg
+ - /Audio/Voice/Zombie/zombie-1.ogg
+ - /Audio/Voice/Zombie/zombie-2.ogg
+ - /Audio/Voice/Zombie/zombie-3.ogg
+ - /Audio/Voice/Vox/shriek1.ogg
name: trait-pacifist-name
components:
- type: Pacifist
+
+- type: trait
+ id: Paracusia
+ name: trait-paracusia-name
+ description: trait-paracusia-desc
+ components:
+ - type: Paracusia
+ minTimeBetweenIncidents: 0.1
+ maxTimeBetweenIncidents: 300
+ maxSoundDistance: 7
+ sounds:
+ collection: Paracusia
+
+- type: trait
+ id: Muted
+ name: trait-muted-name
+ description: trait-muted-desc
+ components:
+ - type: Muted
params:
variation: 0.2
timeBetweenIncidents: 0.3, 300
+
+- type: trait
+ id: LightweightDrunk
+ name: trait-lightweight-name
+ description: trait-lightweight-desc
+ components:
+ - type: LightweightDrunk
+ boozeStrengthMultiplier: 2