A Component representing a heart/heartbeat simulation. It stores BPM, timing math, state (systole/diastole), plays local sounds, exposes properties for debug and controls updating each tick to advance the heartbeat and play systole/diastole SoundEvents.
/*
(c) Perkedel Technologies
GNU GPL v3
*/
namespace Sandbox;
public sealed class HeartOrgan : Component
{
/*
Let's port this from our Godot prototype.
SAUCEs:
- https://github.com/Perkedel/HexagonEngine/blob/4-transitioning/GameDVDCardtridge/DetakJantungProsotipe/DetakJantungProsotipe.gd
- https://github.com/Perkedel/HeartbeatOpenScript-Unity/blob/master/Assets/HeartbeatOpenScript.cs
- https://github.com/godotengine/godot/issues/15895#issuecomment-359185065 OGG sound loop
- https://docs.godotengine.org/en/stable/tutorials/gui/bbcode_in_richtextlabel.html BBCode
Jesus Christ! We made that monolithicly! let's not make the same mistake!
Handmade only!
*/
[Property, Feature("Properties")] public float HeartRate { get; set; } = 70f; // Star of the show!
[Property, Feature("Properties")] public float IdealRate { get; set; } = 70f; // Homo sapiens heart rate ideally is
[Property, Feature( "Properties" )] public float MaximumRate { get; set; } = 500f; // Maximum BPM cap from normal doing
//[Property, Feature("Properties")] public Ecghud ECGMonitor { get; set; } // Insert ECG here!
[Property, Feature( "Sounds" )] public bool EnableSound = true;
[Property, Feature( "Sounds" )] public SoundEvent SystoleSound { get; set; } = ResourceLibrary.Get<SoundEvent>( "sound/dot name/systole.sound" );
[Property, Feature( "Sounds" )] public SoundEvent DiastoleSound { get; set; } = ResourceLibrary.Get<SoundEvent>( "sound/dot name/diastole.sound" );
[Property, Feature( "Sounds" ), RequireComponent] public SoundPointComponent Speaker { get; set; }
[Property, Feature( "Extra Debugs" ), Group( "Critical" ), ReadOnly] protected float CriticalMaxRate { get; set; } = 99999f; // Techical maxium rate from cheated doing
[Header("Here's the Mathematics!")]
[Property, Feature("Extra Debugs"), Group("Mathematics"), ReadOnly] float Hertz { get; set; } = 70f / 60f; // HeartRate / 60
[Property, Feature("Extra Debugs"), Group("Mathematics"), ReadOnly] float PeriodT { get; set; } = 1 / (70 / 60); // 1 / Hertz
[Property, Feature("Extra Debugs"), Group("Running"), ReadOnly] float RemainPeriodT { get; set; } = 1 / (70 / 60); // catch the PeriodT & reset to PeriodT
[Property, Feature("Extra Debugs"), Group("Running Conversion"), ReadOnly] float RemainPeriodTMillisec { get; set; } = (1 / (70 / 60)) * 1000f; // RemainPeriodT * 1000
[Property, Feature("Extra Debugs"), Group("Mathematics"), ReadOnly] float ReturnTime { get; set; } = .25f; // When heart will diastole back?
[Property, Feature("Extra Debugs"), Group("Running"), ReadOnly] float StartReturnTime { get; set; } = .25f; // catch ReturnTime now & reset based ReturnTime
[Property, Feature( "Extra Debugs" ), Group( "Running Conversion" ), ReadOnly] float StartReturnTimeMillisec { get; set; } = (.25f) * 1000f; // StartReturnTime * 1000
[Header("Here's the Core of it!")]
[Property, Feature("Extra Debugs"), Group("Core")] public bool Lub { get; set; } = false; // Heart Systole
[Property, Feature("Extra Debugs"), Group("Core"), ReadOnly] int StateIndex { get; set; } = 0; // Heart organ state
[Property, Feature("Extra Debugs"), Group("Core")] bool isBeating { get; set; } = true; // Heart is alive
[Property, Feature("Extra Debugs"), Group("Info"), ReadOnly] string ToggleSay { get; set; } = ""; // Heart text info
protected void DecideReturnTime( float forWhathowMuch = 0 )
{
if ( forWhathowMuch <= 0 )
{
// you are dead, not a big surprise.
ToggleSay = "X_X Eik Serkat!";
ReturnTime = 0f;
}
else if ( forWhathowMuch >= 1 && forWhathowMuch < 20 )
{
ToggleSay = "...";
ReturnTime = .75f;
}
else if ( forWhathowMuch >= 20 && forWhathowMuch < 50 )
{
ToggleSay = "Looooooooww... heeeaarrt raaaaaate...";
ReturnTime = .5f;
}
else if ( forWhathowMuch >= 50 && forWhathowMuch < 70 )
{
// ToggleSay = "Sleepie";
if ( forWhathowMuch == 69 ) ToggleSay = "nice";
else if ( forWhathowMuch == 67 ) ToggleSay = "bruh";
else ToggleSay = "Sleepie";
ReturnTime = .3f;
}
else if ( forWhathowMuch >= 70 && forWhathowMuch < 90 )
{
ToggleSay = "Heartbeat";
ReturnTime = .25f;
}
else if ( forWhathowMuch >= 90 && forWhathowMuch < 100 )
{
ToggleSay = "Accelerated";
ReturnTime = .20f;
}
else if ( forWhathowMuch >= 100 && forWhathowMuch < 150 )
{
ToggleSay = "FASS";
ReturnTime = .15f;
}
else if ( forWhathowMuch >= 150 && forWhathowMuch < 200 )
{
ToggleSay = "VERY FASS";
ReturnTime = .1f;
}
else if ( forWhathowMuch >= 200 && forWhathowMuch < 300 )
{
ToggleSay = "TOO FASS";
ReturnTime = .05f;
}
else if ( forWhathowMuch >= 300 && forWhathowMuch < 400 )
{
ToggleSay = "EXTREMELY FASS";
ReturnTime = .025f;
}
else if ( forWhathowMuch >= 400 )
{
ToggleSay = "OH PECK!!! FIBRILATION GOING ON!!! OH NO!!!";
ReturnTime = .001f;
}
else
{
ToggleSay = "???";
}
}
public void SetHeartRate( float intoValueOf = 70 )
{
HeartRate = intoValueOf;
if ( HeartRate >= 1 )
{
isBeating = true;
}
else if ( HeartRate <= 0 )
{
Log.Info( "Stop Heartbeat!" );
isBeating = false;
}
DecideReturnTime( intoValueOf );
Log.Info( $"Set HeartRate into {HeartRate} BPM" );
Hertz = HeartRate > 0 ? HeartRate / 60 : 1;
PeriodT = Hertz > 0 ? 1 / Hertz : 1;
}
public void AddHeartRate( float ByWhat = 1 )
{
SetHeartRate( HeartRate + ByWhat );
}
public void ResetHeartRate()
{
// Not to be confused with CPR & Defib!
SetHeartRate( IdealRate );
}
public void ToggleInternalSound()
{
EnableSound = !EnableSound;
}
protected void ResyncHeartUpdate( float Delta )
{
float _Hr = HeartRate;
if ( _Hr >= 1 ) isBeating = true;
else isBeating = false;
if ( _Hr < 0 )
{
HeartRate = 0;
_Hr = 0;
}
if ( _Hr >= CriticalMaxRate)
{
HeartRate = CriticalMaxRate;
_Hr = CriticalMaxRate;
}
DecideReturnTime( _Hr );
Hertz = _Hr > 0 ? _Hr / 60 : 1;
PeriodT = Hertz > 0 ? 1 / Hertz : 1;
}
protected void AsyncHeartbeatUpdate( float Delta )
{
float _capT = Math.Min( RemainPeriodT, 1 / (HeartRate / 60) ); // cap the perioding!
if ( RemainPeriodT > _capT )
{
RemainPeriodT = _capT;
return;
}
RemainPeriodT -= Delta;
RemainPeriodTMillisec = RemainPeriodT * 1000f;
if ( RemainPeriodTMillisec <= 0 )
{
StateIndex = 1;
RemainPeriodT = PeriodT;
RemainPeriodTMillisec = RemainPeriodT * 1000f;
Lub = true;
// TODO: ECG & sound
if(EnableSound){
if ( SystoleSound.IsValid() ) Sound.Play( SystoleSound, WorldPosition );
}
}
else
{
// TODO: ECG
}
if ( Lub )
{
StartReturnTime -= Delta;
StartReturnTimeMillisec = StartReturnTime * 1000f;
if ( StartReturnTimeMillisec <= 0 )
{
StateIndex = 0;
StartReturnTime = ReturnTime;
StartReturnTimeMillisec = StartReturnTime * 1000f;
Lub = false;
// TODO: ECG & sound
if(EnableSound)
{
if ( DiastoleSound.IsValid()) Sound.Play( DiastoleSound, WorldPosition );
}
}
else
{
// TODO: ECG
}
}
}
protected void NoHeartbeatEikSerkat( float Delta )
{
RemainPeriodT = PeriodT;
// Finish everything first!
if ( Lub )
{
StartReturnTime -= Delta;
StartReturnTimeMillisec = StartReturnTime * 1000f;
if ( StartReturnTimeMillisec <= 0 )
{
StateIndex = 0;
StartReturnTime = ReturnTime;
StartReturnTimeMillisec = StartReturnTime * 1000f;
Lub = false;
// TODO: ECG & sound
}
else
{
// TODO: ECG
}
}
}
protected override void OnUpdate()
{
if ( isBeating )
{
AsyncHeartbeatUpdate( Time.Delta );
}
else
{
NoHeartbeatEikSerkat( Time.Delta );
}
ResyncHeartUpdate( Time.Delta );
var _a_ = Game.ActiveScene;
}
}