Фикс и переработка системы сердцебиения (#2098)
This commit is contained in:
parent
4401e83d39
commit
4b97741aba
13 changed files with 207 additions and 68 deletions
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
namespace Content.Server._Sunrise.Heartbeat.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class CritHeartbeatComponent : Component;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
84
Content.Server/_Sunrise/Heartbeat/Systems/HeartbeatSystem.cs
Normal file
84
Content.Server/_Sunrise/Heartbeat/Systems/HeartbeatSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = Проигрывать звук сердцебиения
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue