HeartOrgan component for a s&box game, implementing a simulated heart beat system. It stores heart-rate parameters, timing/period math, hiccup/PVC behavior, sound playback and hooks for an ECG HUD and custom actions on systole.
/*
(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
[Header( "Link another heart organs wirelessly!" )]
[Property, Feature( "Properties" ), Group( "Linking" )] public HeartOrgan[] OtherHearts { get; set; } // Useful for Linked List MultiHearts, or Quantumly Entangled Twins.
[Header( "Adjust Funny effect chances here" )]
[Property, Feature( "Properties" ), Group("Variance"),Advanced] public bool DiastoleReturnDeviates { get; set; } = false; // Is Diastole time deviates?
[Property, Feature( "Properties" ), Group("Variance"),Advanced] public float DiastoleReturnDeviation { get; set; } = .3f; // How much Return Time deviates Plus minus from Decide Return?
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float SkipChancePercent { get; set; } = 0f; // Chance of unexpected extended silence Period Time
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float SkipSetTime { get; set; } = 2f; // and for how long it set instead
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float SkipSetTimeMin { get; set; } = 2f; // Minimum for random decide skip set time
[Property, Feature( "Properties" ), Group( "Hiccup" ),Advanced] public float SkipSetTimeMax { get; set; } = 3f; // & max
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float PVCSetTime { get; set; } = .1f; // and for how long it beats too early per previous PVC session
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float PVCSetTimeMin { get; set; } = .1f; // Minimum for random decide to beats too early
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float PVCSetTimeMax { get; set; } = .3f; // & max
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float PostPVCSetTime { get; set; } = .1f; // and for how long it diastole too early after that skip
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float PostPVCSetTimeMin { get; set; } = .1f; // Minimum for random decide to diastole too early time
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float PostPVCSetTimeMax { get; set; } = .3f; // & max
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public float PVCChancePercent { get; set; } = 0f; // Chance of PVC
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public int PVCCountPerSessionMin { get; set; } = 2; // and how many PVC typically
[Property, Feature( "Properties" ), Group("Hiccup"),Advanced] public int PVCCountPerSessionMax { get; set; } = 2; // and max
//[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] public float PeriodT { get; set; } = 1 / (70 / 60); // 1 / Hertz
[Property, Feature("Extra Debugs"), Group("Running"), ReadOnly] public 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] public float ReturnTime { get; set; } = .25f; // When heart will diastole back?
[Property, Feature("Extra Debugs"), Group("Running"), ReadOnly] public 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
// [Property, Feature( "Extra Debugs" ), Group( "Mathematics" ), ReadOnly] public float PVCPeriodT { get; set; } = 5f; // for PVC event
[Property, Feature( "Extra Debugs" ), Group( "Mathematics" ), ReadOnly] public RealTimeUntil RemainPeriodRTU { get; set; } // s&box Realtime Untils! the Systole
[Property, Feature( "Extra Debugs" ), Group( "Mathematics" ), ReadOnly] public RealTimeUntil StartReturnRTU { get; set; } // and Diastole
[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
/*
- 0
- 1
- 2
- 3
- 10 PVC Lub
- 11 PVC Dub
- 12 Awkward stun / Skip; hmmm use glitched out unexpected increased period time
*/
[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
[Header( "Also Funny effect queue lines" )]
// [Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public uint PrematureVentricularContractions { get; set; } = 0; // Heart had to PVC, pop number
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public uint SkipQueue { get; set; } = 0; // Heart had to skip, pop number
// var DoesPVC ? -> the heart indeed is dadug dadug, not just a pause
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" )] public bool SkipInduced { get; set; } = false; // Manually induce skip
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" )] public bool WithPVC { get; set; } = false; // (argument for Skip) that is dadug-dadug
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" ),ReadOnly] public bool GoingToSkip { get; set; } = false; // buffer for next beat will skip
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public bool SkipActive { get; internal set; } = false; // Heart is skipping, which is a
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public bool SkipDone { get; internal set; } = false; // Heart is skipping, which is a
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public bool IsPVC { get; internal set; } = false; // PVC?, which is Dadug-dadug? false = just a pause
// [Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public bool PVCActive { get; internal set; } = false; // in PVC mode
// [Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public bool PVCDone { get; internal set; } = false; // is it done yet?
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public int PVCNumbersInThisSession { get; set; } = 2; // How many PVC left now
[Property, Feature( "Extra Debugs" ), Group( "Hiccups" ), ReadOnly] public float DiceRollSkip { get; set; } = 0f; // Roll the dice
[Property, Feature("Scripting"), Group("Cookie Clicker")] public Doo OnLub{ get; set; }
[Property, Feature( "Scripting" ), Group( "Rudimentary" )] public Ecghud Ecg { get; set; }
public float NextSingle(float min = 0f, float max = 0f)
{
// https://gist.github.com/MachineCharmer/941949
// http://stackoverflow.com/questions/1064901/random-number-between-2-double-numbers/1064907#1064907
return Game.Random.NextSingle() * (max-min) + min;
}
public void HandoverEcg( Ecghud itThis )
{
Ecg = itThis;
}
protected void OnLubFunc()
{
if ( Ecg.IsValid() )
{
Ecg.CookieClick();
}
}
// protected void DecideLubTime()
// {
// }
public void InduceSkip(bool withPVC = false)
{
WithPVC = withPVC;
GoingToSkip = true;
// SkipInduced = true;
}
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 + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0);
}
else if ( forWhathowMuch >= 20 && forWhathowMuch < 50 )
{
ToggleSay = "Looooooooww... heeeaarrt raaaaaate...";
ReturnTime = .5f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0);
}
else if ( forWhathowMuch >= 50 && forWhathowMuch < 70 )
{
// ToggleSay = "Sleepie";
if ( forWhathowMuch == 69 ) ToggleSay = "nice";
else if ( forWhathowMuch == 67 ) ToggleSay = "bruh";
else ToggleSay = "Sleepie";
ReturnTime = .3f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0);
}
else if ( forWhathowMuch >= 70 && forWhathowMuch < 90 )
{
ToggleSay = "Heartbeat";
ReturnTime = .25f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0);
}
else if ( forWhathowMuch >= 90 && forWhathowMuch < 100 )
{
ToggleSay = "Accelerated";
ReturnTime = .20f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0);
}
else if ( forWhathowMuch >= 100 && forWhathowMuch < 150 )
{
ToggleSay = "FASS";
ReturnTime = .15f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0);
}
else if ( forWhathowMuch >= 150 && forWhathowMuch < 200 )
{
ToggleSay = "VERY FASS";
ReturnTime = .1f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0);
}
else if ( forWhathowMuch >= 200 && forWhathowMuch < 300 )
{
ToggleSay = "TOO FASS";
ReturnTime = .05f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0) * .01f;
}
else if ( forWhathowMuch >= 300 && forWhathowMuch < 400 )
{
ToggleSay = "EXTREMELY FASS";
ReturnTime = .025f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0) * .01f;
}
else if ( forWhathowMuch >= 400 )
{
ToggleSay = "OH PECK!!! FIBRILATION GOING ON!!! OH NO!!!";
ReturnTime = .001f + (DiastoleReturnDeviates? NextSingle(-DiastoleReturnDeviation,DiastoleReturnDeviation) : 0) * .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;
ResyncHeartUpdate(Time.Delta);
}
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;
}
if(!SkipActive || SkipDone)
{
DecideReturnTime( _Hr );
Hertz = _Hr > 0 ? _Hr / 60 : 1;
PeriodT = Hertz > 0 ? 1 / Hertz : 1;
} else
{
Hertz = _Hr > 0 ? _Hr / 60 : 1;
// if(SkipActive && SkipDone)
// {
// SkipActive = false;
// SkipDone = false;
// }
}
}
protected void AsyncHeartInteruptUpdate( float Delta )
{
// Now if there is pending hiccups used, here procedure!
// if ( Lub )
// {
// }
// if(PVCDone)
// {
// // Pop the queue!
// if ( PrematureVentricularContractions > 0 ) PrematureVentricularContractions--; // only pop when is more than 0
// // otherwise you'd roll over to 2^32 whatever this is.
// if ( PrematureVentricularContractions < 0 ) PrematureVentricularContractions = 0; // no I don't think that's how uint work..
// }
}
protected void DazedHeartbeatSubsystem( float Delta)
{
if ( ((SkipChancePercent > 0f && DiceRollSkip < SkipChancePercent) && !SkipActive) || GoingToSkip )
{
// Brainstormed by Joel. that's it. Only a human.
SkipActive = true;
SkipInduced = false;
GoingToSkip = false;
IsPVC = WithPVC;
// decide skip time
// SkipSetTime = Game.Random.Next(SkipSetTimeMin,SkipSetTimeMax);
// SkipSetTime = Game.Random.NextSingle();
// bro
// ~~https://medium.com/analytics-vidhya/random-floats-in-any-range-9b40d30b637b~~
// https://gist.github.com/MachineCharmer/941949
// http://stackoverflow.com/questions/1064901/random-number-between-2-double-numbers/1064907#1064907
// why the fuck no range?!?!?
SkipSetTime = Game.Random.NextSingle() * (SkipSetTimeMax - SkipSetTimeMin) + SkipSetTimeMin;
Log.Info( $"Skip Set time is {SkipSetTime} " );
PVCSetTime = Game.Random.NextSingle() * (PVCSetTimeMax - PVCSetTimeMin) + PVCSetTimeMin;
PostPVCSetTime = Game.Random.NextSingle() * (PostPVCSetTimeMax - PostPVCSetTimeMin) + PostPVCSetTimeMin;
// _capT += 12f;
// _capT = PVCSetTime;
PeriodT = IsPVC? PVCSetTime : SkipSetTime;
RemainPeriodT = IsPVC ? PVCSetTime : SkipSetTime;
RemainPeriodRTU = IsPVC ? PVCSetTime : SkipSetTime;
ReturnTime = IsPVC? PostPVCSetTime : ReturnTime;
// StartReturnTime = PostPVCSetTime;
// if ( SkipQueue > 0 ) SkipQueue--;
// if ( SkipQueue < 0 ) SkipQueue = 0;
PVCNumbersInThisSession = Game.Random.Next( PVCCountPerSessionMin, PVCCountPerSessionMax );
}
else
{
if ( SkipActive )
{
}
}
// return void;
}
protected void AsyncHeartbeatUpdate( float Delta )
{
float _capT = Math.Min( RemainPeriodT, 1 / (HeartRate / 60) ); // cap the perioding!
// but if heart had to skip a beat
DazedHeartbeatSubsystem( Delta );
if(SkipActive)
{
_capT = RemainPeriodT;
}
if ( RemainPeriodT > _capT )
{
RemainPeriodT = _capT;
RemainPeriodRTU = _capT;
return;
}
if (RemainPeriodT < 0)
{
RemainPeriodT = 0;
RemainPeriodRTU = 0;
return;
}
RemainPeriodT -= Delta;
RemainPeriodTMillisec = RemainPeriodT * 1000f;
if ( RemainPeriodTMillisec <= 0 )
{
// But before systole, check if there is Hiccup on queue
// if(PrematureVentricularContractions > 0)
// {
// // interupt & go to this special procedure instead!
// // return
// }
// Heart Systole
StateIndex = 1;
RemainPeriodT = PeriodT;
RemainPeriodRTU = PeriodT;
RemainPeriodTMillisec = RemainPeriodT * 1000f;
Lub = true;
// DONE: ECG & sound
if ( EnableSound )
{
if ( SystoleSound.IsValid() ) Sound.Play( SystoleSound, WorldPosition );
}
RunDoo( OnLub );
OnLubFunc();
if(SkipActive)
{
if ( IsPVC )
// check count phase
if(PVCNumbersInThisSession > 0)
{
// ongoing count
PVCSetTime = Game.Random.NextSingle() * (PVCSetTimeMax - PVCSetTimeMin) + PVCSetTimeMin;
PostPVCSetTime = Game.Random.NextSingle() * (PostPVCSetTimeMax - PostPVCSetTimeMin) + PostPVCSetTimeMin;
// _capT = PVCSetTime;
PeriodT = PVCSetTime;
// RemainPeriodT = PVCSetTime;
ReturnTime = PostPVCSetTime;
if ( PVCNumbersInThisSession > 0 ) PVCNumbersInThisSession--;
if ( PVCNumbersInThisSession < 0 ) PVCNumbersInThisSession = 0;
} else
{
// this is the last one!
// SkipSetTime = Game.Random.NextSingle() * (SkipSetTimeMax - SkipSetTimeMin) + SkipSetTimeMin;
// _capT = SkipSetTime;
PeriodT = SkipSetTime;
// RemainPeriodT = SkipSetTime;
// so it pauses really
// SkipActive = false;
SkipDone = true;
}
else
{
// Just a pause then? okay let's call it immediately.
// _capT = SkipSetTime;
PeriodT = SkipSetTime;
// RemainPeriodT = SkipSetTime;
SkipDone = true;
}
}
}
else
{
// DONE: ECG
}
if ( Lub )
{
StartReturnTime -= Delta;
StartReturnTimeMillisec = StartReturnTime * 1000f;
if ( StartReturnTimeMillisec <= 0 )
{
// Heart Diastole
StateIndex = 0;
StartReturnTime = ReturnTime;
StartReturnRTU = ReturnTime; // NEW
StartReturnTimeMillisec = StartReturnTime * 1000f;
Lub = false;
// DONE: ECG & sound
if ( EnableSound )
{
if ( DiastoleSound.IsValid() ) Sound.Play( DiastoleSound, WorldPosition );
}
if(SkipActive)
{
if(IsPVC && PVCNumbersInThisSession == 1)
{
// the last PVC before "null" terminator (0), try go to pause
PeriodT = SkipSetTime;
// RemainPeriodT = PeriodT;
// RemainPeriodRTU = PeriodT;
}
if(SkipActive && SkipDone)
{
PeriodT = SkipSetTime;
RemainPeriodT = PeriodT;
RemainPeriodRTU = PeriodT;
SkipActive = false;
SkipDone = false;
// heart got it together again
// ResyncHeartUpdate( Delta );
}
} else
{
// check if heart will skip
// if ( (( SkipChancePercent > 0f && DiceRollSkip < SkipChancePercent ) && !SkipActive) || SkipInduced )
// {
// DazedHeartbeatSubsystem( Delta );
// }
if ( GoingToSkip ) SkipActive = true;
// Refresh Remain Period T, partial resync
// RemainPeriodT = PeriodT;
// dice roll for next beat
DiceRollSkip = Game.Random.NextSingle() * 100f;
}
}
else
{
// DONE: 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;
StartReturnRTU = ReturnTime;
StartReturnTimeMillisec = StartReturnTime * 1000f;
Lub = false;
// TODO: ECG & sound
}
else
{
// TODO: ECG
}
}
}
protected override void OnStart()
{
// Check the Doo
}
protected override void OnUpdate()
{
if ( isBeating )
{
AsyncHeartbeatUpdate( Time.Delta );
}
else
{
NoHeartbeatEikSerkat( Time.Delta );
}
ResyncHeartUpdate( Time.Delta );
var _a_ = Game.ActiveScene;
}
}