Фикс и переработка системы сердцебиения (#2098)

This commit is contained in:
ThereDrD 2025-05-07 03:27:36 +03:00 committed by GitHub
parent 4401e83d39
commit 4b97741aba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 207 additions and 68 deletions

View file

@ -31,6 +31,7 @@
<CheckBox Name="JumpSoundDisableCheckBox" Text="{Loc 'ui-options-jump-sound-disable'}" />
<CheckBox Name="VoteMusicDisableCheckBox" Text="{Loc 'ui-options-vote-music-disable'}" />
<CheckBox Name="MuteGhostRoleNotificationCheckBox" Text="{Loc 'ui-options-mute-new-ghost-roles'}" />
<CheckBox Name="PlayHeartbeatSound" Text="{Loc 'ui-options-play-heartbeat-sound'}" />
<!-- Graphics -->
<Label Text="{Loc 'ui-options-sunrise-general-graphics'}" StyleClasses="LabelKeyText"/>

View file

@ -43,6 +43,8 @@ public sealed partial class ExtraTab : Control
Control.AddOptionCheckBox(SunriseCCVars.VoteMusicDisable, VoteMusicDisableCheckBox);
Control.AddOptionCheckBox(SunriseCCVars.MuteGhostRoleNotification, MuteGhostRoleNotificationCheckBox);
Control.AddOptionCheckBox(SunriseCCVars.PlayHeartBeatSound, PlayHeartbeatSound);
_cfg.OnValueChanged(SunriseCCVars.LobbyBackgroundType, OnLobbyBackgroundTypeChanged, true);
var lobbyBackgroundTypes = new List<OptionDropDownCVar<string>.ValueOption>

View file

@ -0,0 +1,29 @@
using Content.Shared._Sunrise.Heartbeat;
using Content.Shared._Sunrise.SunriseCCVars;
using Robust.Shared.Configuration;
namespace Content.Client._Sunrise.Heartbeat;
public sealed class HeartbeatSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override void Initialize()
{
base.Initialize();
_cfg.OnValueChanged(SunriseCCVars.PlayHeartBeatSound, OnOptionsChanged, true);
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(SunriseCCVars.PlayHeartBeatSound, OnOptionsChanged);
}
private void OnOptionsChanged(bool option)
{
RaiseNetworkEvent(new HeartbeatOptionsChangedEvent(option));
}
}

View file

@ -1,18 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server._Sunrise.CritHeartbeat;
[RegisterComponent]
public sealed partial class CritHeartbeatComponent : Component
{
[DataField]
public SoundSpecifier HeartbeatSound = new SoundPathSpecifier("/Audio/_Sunrise/Effects/heartbeat.ogg");
/// <summary>
/// Чтобы выключать это для наследников в прототипах
/// </summary>
[DataField]
public bool Enabled = true;
public EntityUid? AudioStream;
}

View file

@ -1,45 +0,0 @@
using Content.Shared.Damage;
using Content.Shared.Mobs;
using Robust.Server.Audio;
using Robust.Shared.Audio;
namespace Content.Server._Sunrise.CritHeartbeat;
public sealed class CritHeartbeatSystem : EntitySystem
{
[Dependency] private readonly AudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CritHeartbeatComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<CritHeartbeatComponent, DamageChangedEvent>(OnDamage);
}
private void OnMobStateChanged(Entity<CritHeartbeatComponent> ent, ref MobStateChangedEvent args)
{
if (!ent.Comp.Enabled)
return;
ent.Comp.AudioStream = args.NewMobState == MobState.Critical
? _audio.PlayEntity(ent.Comp.HeartbeatSound, ent, ent)?.Entity
: _audio.Stop(ent.Comp.AudioStream);
}
private void OnDamage(Entity<CritHeartbeatComponent> ent, ref DamageChangedEvent args)
{
if (!ent.Comp.Enabled)
return;
if (!Exists(ent.Comp.AudioStream))
return;
var pitch = Math.Min(1, 100 / args.Damageable.TotalDamage.Float());
// Потому что игра говно, тут нельзя изменять аудиопарамс уже существующего звука. Поэтому я пересоздаю его заново
// Это приводит к проигрыванию звука через неравномерные промежутки времени, но зато работает и не очень заметно
_audio.Stop(ent.Comp.AudioStream);
ent.Comp.AudioStream = _audio.PlayEntity(ent.Comp.HeartbeatSound, ent, ent, AudioParams.Default.WithPitchScale(pitch))?.Entity;
}
}

View file

@ -0,0 +1,10 @@
namespace Content.Server._Sunrise.Heartbeat.Components;
[RegisterComponent]
public sealed partial class ActiveHeartbeatComponent : Component
{
[ViewVariables] public float Pitch = 1f;
[ViewVariables] public TimeSpan NextHeartbeatCooldown = TimeSpan.FromSeconds(0.5f);
public TimeSpan? NextHeartbeatTime;
}

View file

@ -0,0 +1,4 @@
namespace Content.Server._Sunrise.Heartbeat.Components;
[RegisterComponent]
public sealed partial class CritHeartbeatComponent : Component;

View file

@ -0,0 +1,60 @@
using Content.Server._Sunrise.Heartbeat.Components;
using Content.Shared.Damage;
using Content.Shared.Mobs;
namespace Content.Server._Sunrise.Heartbeat.Systems;
public sealed partial class HeartbeatSystem
{
// Минимальное и максимальное время между ударами сердца
private const float MinimumCooldown = 0.5f;
private const float MaximumCooldown = 3f;
private void OnMobStateChanged(Entity<CritHeartbeatComponent> ent, ref MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Critical)
{
RemComp<ActiveHeartbeatComponent>(ent);
return;
}
var activeHeartbeat = EnsureComp<ActiveHeartbeatComponent>(ent);
TryCalculateCurrentState((ent.Owner, activeHeartbeat));
SetNextTime(activeHeartbeat);
}
/// <summary>
/// Подтягивает значения эффектов в зависимости от того, насколько игрок продамажен
/// Чем выше урон -> тем медленнее бьется сердце и тем более глухой звук
/// </summary>
private void OnDamage(Entity<ActiveHeartbeatComponent> ent, ref DamageChangedEvent args)
{
TryCalculateCurrentState(ent, args.Damageable);
}
/// <summary>
/// Подсчитывает нужные данные о текущем уроне тела и в зависимости от них задает нужный pitch и cooldown для сердцебиения
/// </summary>
/// <param name="ent"></param>
/// <param name="damageable"></param>
/// <returns></returns>
private bool TryCalculateCurrentState(Entity<ActiveHeartbeatComponent> ent, DamageableComponent? damageable = null)
{
if (!Resolve(ent.Owner, ref damageable))
return false;
var totalDamage = damageable.TotalDamage.Float();
var pitch = Math.Min(1f, 100f / totalDamage);
var excess = Math.Max(0f, totalDamage - 100f);
var cooldownSeconds = MinimumCooldown + (excess / 100f) * (MaximumCooldown - MinimumCooldown);
ent.Comp.Pitch = pitch;
ent.Comp.NextHeartbeatCooldown = TimeSpan.FromSeconds(cooldownSeconds);
return true;
}
}

View file

@ -0,0 +1,84 @@
using Content.Server._Sunrise.Heartbeat.Components;
using Content.Shared._Sunrise.Heartbeat;
using Content.Shared.Damage;
using Content.Shared.GameTicking;
using Content.Shared.Mobs;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._Sunrise.Heartbeat.Systems;
// TODO: Сделать возможность с помощью стетоскопа услышать сердцебиение человека
public sealed partial class HeartbeatSystem : EntitySystem
{
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private static readonly SoundSpecifier HeartbeatSound =
new SoundPathSpecifier("/Audio/_Sunrise/Effects/heartbeat.ogg", AudioParams.Default.WithVolume(-3f));
private static readonly HashSet<ICommonSession> DisabledSessions = [];
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CritHeartbeatComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<ActiveHeartbeatComponent, DamageChangedEvent>(OnDamage);
SubscribeNetworkEvent<HeartbeatOptionsChangedEvent>(OnOptionsChanged);
SubscribeLocalEvent<RoundRestartCleanupEvent>(_ => DisabledSessions.Clear());
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ActiveHeartbeatComponent>();
while (query.MoveNext(out var uid, out var activeHeartbeat))
{
if (_timing.CurTime < activeHeartbeat.NextHeartbeatTime)
continue;
if (IsDisabledByClient(uid))
continue;
_audio.PlayGlobal(HeartbeatSound, uid, AudioParams.Default.WithPitchScale(activeHeartbeat.Pitch));
SetNextTime(activeHeartbeat);
}
}
/// <summary>
/// Устанавливает время следующего удара сердца
/// </summary>
private void SetNextTime(ActiveHeartbeatComponent component)
{
component.NextHeartbeatTime = _timing.CurTime + component.NextHeartbeatCooldown;
}
private bool IsDisabledByClient(EntityUid player)
{
if (!_player.TryGetSessionByEntity(player, out var session))
return true;
if (DisabledSessions.Contains(session))
return true;
return false;
}
private static void OnOptionsChanged(HeartbeatOptionsChangedEvent ev, EntitySessionEventArgs args)
{
if (ev.Enabled)
DisabledSessions.Remove(args.SenderSession);
else
DisabledSessions.Add(args.SenderSession);
}
}

View file

@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Heartbeat;
[Serializable, NetSerializable]
public sealed class HeartbeatOptionsChangedEvent(bool enabled) : EntityEventArgs
{
public bool Enabled { get; } = enabled;
}

View file

@ -428,4 +428,11 @@ public sealed partial class SunriseCCVars : CVars
public static readonly CVarDef<bool> MuteGhostRoleNotification =
CVarDef.Create("ghost.mute_role_notification", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* Heartbeat sound
*/
public static readonly CVarDef<bool> PlayHeartBeatSound =
CVarDef.Create("heartbeat.play_sound", true, CVar.CLIENTONLY | CVar.ARCHIVE);
}

View file

@ -24,3 +24,4 @@ ui-options-function-auto-get-up = Автоматически вставать п
ui-options-function-hold-look-up = Удерживать клавишу для прицеливания
ui-options-chat-icons-enable = Использовать иконки профессий в чате
ui-options-chat-pointing-visuals-enable = Отображать указывания с иконками в чате
ui-options-play-heartbeat-sound = Проигрывать звук сердцебиения

View file

@ -270,11 +270,6 @@
- type: CanEscapeInventory
- type: Mood
- type: CritHeartbeat
heartbeatSound:
path: /Audio/_Sunrise/Effects/heartbeat.ogg
params:
volume: -3
loop: True
# Sunrise-End
- type: Barotrauma
damage: