diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 776c79c..5fc7318 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -23,28 +23,31 @@ jobs:
- name: Restore dependencies
run: dotnet restore
- - name: Build Release (library)
- run: dotnet build GEffectsLogic/GEffectsLogic.csproj -c Release --no-restore
+ - name: Build all configurations
+ run: |
+ dotnet build GEffectsLogic/GEffectsLogic.csproj -c Release --no-restore
+ dotnet build GEffectsLogic/GEffectsLogic.csproj -c Debug --no-restore
- - name: Build Debug (library)
- run: dotnet build GEffectsLogic/GEffectsLogic.csproj -c Debug --no-restore
+ - name: Collect artifacts
+ run: |
+ mkdir -p artifacts/net10.0/release
+ mkdir -p artifacts/net10.0/debug
+ mkdir -p artifacts/net481/release
+ mkdir -p artifacts/net481/debug
- - name: Upload Release DLL artifact
- uses: actions/upload-artifact@v4
- with:
- name: GEffectsLogic.dll
- path: GEffectsLogic/bin/Release/net10.0/GEffectsLogic.dll
+ cp GEffectsLogic/bin/Release/net10.0/GEffectsLogic.dll artifacts/net10.0/release/
+ cp GEffectsLogic/bin/Debug/net10.0/GEffectsLogic.dll artifacts/net10.0/debug/
+ cp GEffectsLogic/bin/Debug/net10.0/GEffectsLogic.pdb artifacts/net10.0/debug/
- - name: Prepare Debug DLL name
- run: |
- mkdir -p artifacts/debug
- cp GEffectsLogic/bin/Debug/net10.0/GEffectsLogic.dll artifacts/debug/GEffectsLogic.dev.dll
+ cp GEffectsLogic/bin/Release/net481/GEffectsLogic.dll artifacts/net481/release/
+ cp GEffectsLogic/bin/Debug/net481/GEffectsLogic.dll artifacts/net481/debug/
+ cp GEffectsLogic/bin/Debug/net481/GEffectsLogic.pdb artifacts/net481/debug/
- - name: Upload Debug DLL artifact
+ - name: Upload artifacts
uses: actions/upload-artifact@v4
with:
- name: GEffectsLogic.dev.dll
- path: artifacts/debug/GEffectsLogic.dev.dll
+ name: GEffectsLogic
+ path: artifacts/
test:
runs-on: ubuntu-latest
diff --git a/.idea/.idea.GEffectsLogic/.idea/workspace.xml b/.idea/.idea.GEffectsLogic/.idea/workspace.xml
index 681b253..3291e68 100644
--- a/.idea/.idea.GEffectsLogic/.idea/workspace.xml
+++ b/.idea/.idea.GEffectsLogic/.idea/workspace.xml
@@ -5,22 +5,11 @@
GEffectsLogic/GEffectsLogic.csproj
GraphicLogicTest/GraphicLogicTest.csproj
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -30,8 +19,18 @@
+
+
+
+
+
+
{
"associatedIndex": 0,
@@ -42,25 +41,32 @@
- {
+ "keyToString": {
+ ".NET Project.GraphicLogicTest.executor": "Run",
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
+ "RunOnceActivity.cidr.known.project.marker": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.readMode.enableVisualFormatting": "true",
+ "RunOnceActivity.typescript.service.memoryLimit.init": "true",
+ "XThreadsFramesViewSplitterKey": "0.3137255",
+ "cidr.known.project.marker": "true",
+ "codeWithMe.voiceChat.enabledByDefault": "false",
+ "git-widget-placeholder": "develop-GLoCPlot",
+ "git.auto.fetch.suggestion.counter": "1",
+ "last_opened_file_path": "/Users/LINBASS/repos/GEffectsLogic",
+ "nodejs_package_manager_path": "npm",
+ "settings.editor.selected.configurable": "vcs.Git",
+ "vue.rearranger.settings.migration": "true"
+ },
+ "keyToStringList": {
+ "com.intellij.ide.scratch.ScratchImplUtil$2/New Scratch File": [
+ "TEXT"
+ ]
}
-}]]>
+}
@@ -126,7 +132,38 @@
+
+
+
+
+
+
+
+
+
+ 1782282281457
+
+
+
+ 1782282281457
+
+
+ 1782282890523
+
+
+
+ 1782282890523
+
+
+
+ 1782286897653
+
+
+
+ 1782286897653
+
+
@@ -134,8 +171,23 @@
+
+
+
+
+
+
+
diff --git a/GEffectsLogic/GEffectsLogic.csproj b/GEffectsLogic/GEffectsLogic.csproj
index 0a2111e..a8429f5 100644
--- a/GEffectsLogic/GEffectsLogic.csproj
+++ b/GEffectsLogic/GEffectsLogic.csproj
@@ -1,15 +1,15 @@
- net10.0
- enable
- enable
+ net481;net10.0
+ latest
+ enable
Debug;Release;PerfDebug
False
- 0.2.0
+ 0.3.0
$(VersionPrefix)
- 0.2.0.0
+ 0.3.0.0
$(VersionPrefix).0
nogit
diff --git a/GEffectsLogic/LogicSettings.cs b/GEffectsLogic/LogicSettings.cs
index 3dc2d8f..c38562e 100644
--- a/GEffectsLogic/LogicSettings.cs
+++ b/GEffectsLogic/LogicSettings.cs
@@ -2,7 +2,7 @@
{
public static class LogicSettings
{
- public static double StabilizationTimeThreshold { get; set; } = 30.0;
+ public static double StabilizationTimeThreshold { get; set; } = 600.0;
// --- Physiological model parameters ---
@@ -16,7 +16,6 @@ public static class LogicSettings
public static double HydrostaticShiftExponent { get; set; } = 2.2;
// Keep these summing to ~1.0
- public static double HeadCoreShiftFraction { get; set; } = 0.45;
public static double CoreLowerShiftFraction { get; set; } = 0.55;
// Passive return / compensation
@@ -98,5 +97,26 @@ public static class LogicSettings
public static double GSuitGlobalShiftReductionMax { get; set; } = 0.20; // optional mild global scaling
public static double GSuitCoreLowerReductionMax { get; set; } = 0.60; // reduce core->lower pooling
public static double GSuitLowerReturnBoostMax { get; set; } = 0.80; // increase lower return
+
+ // --- Fatigue / resistance reduction ---
+
+ // Fraction of GSuitEffectiveness that the suit retains passively (hardware inflation) regardless of fatigue
+ public static double GSuitPassiveFraction { get; set; } = 0.25;
+
+ // Straining (AGSM) fatigue: rate at which the human straining component degrades
+ // Build rate is per-second at strainingLevel=1 (quadratic: actual rate = BuildRate * strainingLevel²)
+ // ~60-90s of max straining to saturate (1/BuildRate ≈ saturation time)
+ public static double StrainingFatigueBuildRate { get; set; } = 0.015; // saturates ~67s at full strain
+ public static double StrainingFatigueRecoveryTau { get; set; } = 150.0; // ~2.5 min to recover
+
+ // G-suit mechanical fatigue: slower than straining fatigue (suit outlasts the pilot's AGSM)
+ // Build rate is per-second at strainingLevel=1 (linear: actual rate = BuildRate * strainingLevel)
+ public static double GSuitFatigueBuildRate { get; set; } = 0.004; // saturates ~250s at full strain
+ public static double GSuitFatigueRecoveryTau { get; set; } = 300.0; // ~5 min to recover
+
+ // Cardiovascular fatigue: hrFatigue (0..1) accumulates at CardioFatigueBuildRate × hrElevation per second
+ public static double CardioFatigueBuildRate { get; set; } = 0.008; // ~125s at max HR elevation to fully fatigue
+ public static double CardioFatigueRecoveryTau { get; set; } = 240.0; // ~4 min to fully recover
+ public static double CardioFatigueMaxHRFloor { get; set; } = 1.25; // HR floor when fully fatigued
}
}
\ No newline at end of file
diff --git a/GEffectsLogic/PhysiologicalModel.cs b/GEffectsLogic/PhysiologicalModel.cs
index 6df7579..cbd5a56 100644
--- a/GEffectsLogic/PhysiologicalModel.cs
+++ b/GEffectsLogic/PhysiologicalModel.cs
@@ -24,15 +24,17 @@ namespace GEffectsLogic
///
public class PhysiologicalModel
{
- // TODO - fix straining level being a fixed input
-
// --- Compartment blood volumes (fraction of total, sum = 1.0) ---
protected double bloodHead = LogicSettings.RestingBloodHead;
protected double bloodCore = LogicSettings.RestingBloodCore;
protected double bloodLower = LogicSettings.RestingBloodLower;
protected double brainO2 = 1.0;
protected double heartRateMultiplier = 1.0; // Baroreceptor reflex: heart rate multiplier (1.0 = resting)
- protected double strainingLevel = 0.0; // Straining effort (0..1): pilot anti-G straining maneuver including g suit inflation
+ protected double strainingLevel = 0.0; // Straining effort (0..1): pilot anti-G straining maneuver including g-suit inflation
+ protected double strainingFatigue = 0.0; // Accumulated AGSM fatigue (0..1): degrades the human straining component
+ protected double gSuitFatigue = 0.0; // Accumulated g-suit fatigue (0..1): degrades mechanical suit compression
+ protected double hrFatigue = 0.0; // Cardiovascular fatigue (0..1): suppresses baroreceptor HR toward fatigueHeartRateFloor
+ protected double fatigueHeartRateFloor = LogicSettings.CardioFatigueMaxHRFloor;
protected double perfusionLevel = 0.0;
protected double consciousnessLevel = 1.0;
//protected double confusionLevel = 0.0;
@@ -42,7 +44,7 @@ public class PhysiologicalModel
#region Public read-only state
public readonly int UniqueID;
-
+
/// Fraction of total blood in the head compartment.
public double BloodHead => bloodHead;
@@ -58,6 +60,18 @@ public class PhysiologicalModel
/// Current heart rate multiplier from baroreceptor reflex.
public double HeartRateMultiplier => heartRateMultiplier;
+ /// Accumulated AGSM fatigue (0 = fresh, 1 = fully exhausted).
+ public double StrainingFatigue => strainingFatigue;
+
+ /// Accumulated g-suit mechanical fatigue (0 = full effectiveness, 1 = degraded to passive fraction).
+ public double GSuitFatigue => gSuitFatigue;
+
+ /// Cardiovascular fatigue accumulator (0 = fresh, 1 = fully fatigued — HR pinned to floor).
+ public double HrFatigue => hrFatigue;
+
+ /// Fixed HR floor the baroreceptor is suppressed toward when fully fatigued.
+ public double FatigueHeartRateFloor => fatigueHeartRateFloor;
+
/// Current straining level (0 = none, 1 = max).
//public double StrainingLevel => strainingLevel;
@@ -88,6 +102,10 @@ public virtual void Reset()
brainO2 = 1.0;
heartRateMultiplier = 1.0;
strainingLevel = 0.0;
+ strainingFatigue = 0.0;
+ gSuitFatigue = 0.0;
+ hrFatigue = 0.0;
+ fatigueHeartRateFloor = LogicSettings.CardioFatigueMaxHRFloor;
tunnelVisionLevel = 0.0;
greyScaleLevel = 0.0;
primaryColor = true;
@@ -95,9 +113,16 @@ public virtual void Reset()
perfusionLevel = 0.0;
}
+#if NET481
+ public static double Clamp(double value, double min, double max) => Math.Max(Math.Min(value, max), min);
+#else
+ public static double Clamp(double value, double min, double max) => Math.Clamp(value, min, max);
+#endif
+
+
public static double SmoothStep(double x)
{
- x = Math.Clamp(x, 0.0, 1.0);
+ x = Clamp(x, 0.0, 1.0);
return x * x * (3.0 - (2.0 * x));
}
@@ -145,17 +170,41 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
targetStraining = (gz - LogicSettings.StrainingStartGz) /
(LogicSettings.StrainingFullGz - LogicSettings.StrainingStartGz);
}
- targetStraining = Math.Clamp(targetStraining, 0.0, 1.0);
+ targetStraining = Clamp(targetStraining, 0.0, 1.0);
strainingLevel = StepTowardsLinear(strainingLevel, targetStraining, LogicSettings.StrainingTau, dt);
- // Suit effect only for +Gz loading
- double suitActivation = Math.Clamp(LogicSettings.GSuitEffectiveness * strainingLevel, 0.0, 1.0);
+ // Fatigue: straining fatigue fills while strainingLevel is high, drains slowly at rest
+ double strainingFatigueBuildRate = LogicSettings.StrainingFatigueBuildRate * strainingLevel * strainingLevel;
+ double strainingFatigueDecayRate = 1.0 / LogicSettings.StrainingFatigueRecoveryTau;
+ strainingFatigue += strainingLevel > 0.01
+ ? strainingFatigueBuildRate * dt
+ : -strainingFatigueDecayRate * strainingFatigue * dt;
+ strainingFatigue = Clamp(strainingFatigue, 0.0, 1.0);
+
+ // G-suit fatigue builds more slowly (mechanical, outlasts the pilot's AGSM), also drains slowly
+ double gSuitFatigueBuildRate = LogicSettings.GSuitFatigueBuildRate * strainingLevel;
+ double gSuitFatigueDecayRate = 1.0 / LogicSettings.GSuitFatigueRecoveryTau;
+ gSuitFatigue += strainingLevel > 0.01
+ ? gSuitFatigueBuildRate * dt
+ : -gSuitFatigueDecayRate * gSuitFatigue * dt;
+ gSuitFatigue = Clamp(gSuitFatigue, 0.0, 1.0);
+
+ // Effective straining: human AGSM component fully degrades with strainingFatigue
+ double effectiveStraining = strainingLevel * (1.0 - strainingFatigue);
+
+ // Effective g-suit: mechanical suit retains a passive fraction, only the active compression degrades
+ double gSuitActiveFraction = 1.0 - LogicSettings.GSuitPassiveFraction;
+ double effectiveGSuit = LogicSettings.GSuitEffectiveness *
+ (LogicSettings.GSuitPassiveFraction + (gSuitActiveFraction * (1.0 - gSuitFatigue)));
+
+ // Suit effect only for +Gz loading, coupled to effective straining
+ double suitActivation = Clamp(effectiveGSuit * effectiveStraining, 0.0, 1.0);
double suit = gzNet > 0.0 ? suitActivation : 0.0;
// Mild global scaling + targeted redistribution
double effectiveGzShift = gzNetScaled * (1.0 - (LogicSettings.GSuitGlobalShiftReductionMax * suit));
- double coreLowerFractionEffective = Math.Clamp(
+ double coreLowerFractionEffective = Clamp(
LogicSettings.CoreLowerShiftFraction * (1.0 - (LogicSettings.GSuitCoreLowerReductionMax * suit)),
0.05, 0.95);
@@ -171,9 +220,9 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
double returnRate = LogicSettings.PassiveReturnRate * heartRateMultiplier;
double lowerReturnRate = returnRate * (1.0 + (LogicSettings.GSuitLowerReturnBoostMax * suit));
- bloodHead = (bloodHead + (shiftHeadRate + (returnRate * LogicSettings.RestingBloodHead)) * dt) / (1.0 + (returnRate * dt));
- bloodCore = (bloodCore + (shiftCoreRate + (returnRate * LogicSettings.RestingBloodCore)) * dt) / (1.0 + (returnRate * dt));
- bloodLower = (bloodLower + (shiftLowerRate + (lowerReturnRate * LogicSettings.RestingBloodLower)) * dt) / (1.0 + (lowerReturnRate * dt));
+ bloodHead = (bloodHead + ((shiftHeadRate + (returnRate * LogicSettings.RestingBloodHead)) * dt)) / (1.0 + (returnRate * dt));
+ bloodCore = (bloodCore + ((shiftCoreRate + (returnRate * LogicSettings.RestingBloodCore)) * dt)) / (1.0 + (returnRate * dt));
+ bloodLower = (bloodLower + ((shiftLowerRate + (lowerReturnRate * LogicSettings.RestingBloodLower)) * dt)) / (1.0 + (lowerReturnRate * dt));
// Enforce conservation (redistribute any numerical drift)
double total = bloodHead + bloodCore + bloodLower;
@@ -213,23 +262,23 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
// O2 delivery depends on head blood volume relative to resting
double perfusionRatio = bloodHead / LogicSettings.RestingBloodHead;
- perfusionRatio = Math.Clamp(perfusionRatio, 0.0, 1.0);
+ perfusionRatio = Clamp(perfusionRatio, 0.0, 1.0);
// Perfusion shaping
double s = LogicSettings.O2PerfusionCurveStrength;
double pivot = LogicSettings.O2PerfusionCurvePivot;
double shapedPerfusion =
perfusionRatio - (s * perfusionRatio * (1.0 - perfusionRatio) * (perfusionRatio - pivot));
- shapedPerfusion = Math.Clamp(shapedPerfusion, 0.0, 1.0);
+ shapedPerfusion = Clamp(shapedPerfusion, 0.0, 1.0);
// convert perfusion -> effective O2 delivery (non-linear + mild sustained hypoperfusion penalty)
double effectiveDelivery = Math.Pow(shapedPerfusion, LogicSettings.BrainO2PerfusionExponent);
- double threshold = Math.Clamp(LogicSettings.BrainO2HypoperfusionThreshold, 0.01, 1.0);
+ double threshold = Clamp(LogicSettings.BrainO2HypoperfusionThreshold, 0.01, 1.0);
double hypoperfusion = Math.Max(0.0, threshold - shapedPerfusion) / threshold;
double hypoperfusionPenalty = LogicSettings.BrainO2HypoperfusionPenaltyStrength * hypoperfusion * hypoperfusion;
- effectiveDelivery = Math.Clamp(effectiveDelivery - hypoperfusionPenalty, 0.0, 1.0);
+ effectiveDelivery = Clamp(effectiveDelivery - hypoperfusionPenalty, 0.0, 1.0);
// Target O2 is bounded by floor, then approached with time constants
double targetBrainO2 = LogicSettings.BrainO2Floor + ((1.0 - LogicSettings.BrainO2Floor) * effectiveDelivery);
@@ -245,11 +294,25 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
: LogicSettings.BrainO2RecoveryTau;
brainO2 = StepTowardsLinear(brainO2, targetBrainO2, o2Tau, dt);
- brainO2 = Math.Clamp(brainO2, LogicSettings.BrainO2Floor, 1.0);
-
- // When head perfusion drops, heart rate increases (delayed response)
- double targetHR = 1.0 + (LogicSettings.BaroreceptorGain * Math.Max(0, 1.0 - perfusionRatio));
- targetHR = Math.Min(targetHR, LogicSettings.MaxHeartRateMultiplier);
+ brainO2 = Clamp(brainO2, LogicSettings.BrainO2Floor, 1.0);
+
+ // Cardiovascular fatigue: hrFatigue (0..1) accumulates with time × HR elevation,
+ // decays slowly at rest. It is independent of current HR so it always wins eventually.
+ double hrElevation = Math.Max(0.0, heartRateMultiplier - 1.0);
+ double hrFatigueBuildRate = LogicSettings.CardioFatigueBuildRate * hrElevation;
+ hrFatigue += hrElevation > 0.05
+ ? hrFatigueBuildRate * dt
+ : -(hrFatigue / LogicSettings.CardioFatigueRecoveryTau) * dt;
+ hrFatigue = Clamp(hrFatigue, 0.0, 1.0);
+
+ // fatigueHeartRateFloor is a fixed setting: the resting HR offset the cardiovascular
+ // system is stuck at once fully fatigued (independent of feedback).
+ // Baroreceptor target from current perfusion deficit.
+ double baroTarget = 1.0 + (LogicSettings.BaroreceptorGain * Math.Max(0, 1.0 - perfusionRatio));
+ baroTarget = Math.Min(baroTarget, LogicSettings.MaxHeartRateMultiplier);
+
+ // hrFatigue suppresses baroreceptor response: at hrFatigue=1 the target is pinned to the floor.
+ double targetHR = baroTarget + (hrFatigue * (fatigueHeartRateFloor - baroTarget));
double hrTau = LogicSettings.BaroreceptorTimeConstant;
heartRateMultiplier = StepTowardsLinear(heartRateMultiplier, targetHR, hrTau, dt);
@@ -258,15 +321,15 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
primaryColor = bloodHead <= LogicSettings.RestingBloodHead;
// Map brain O2 to consciousness via smooth step
- double o2Normalized = Math.Clamp(
+ double o2Normalized = Clamp(
(BrainO2 - LogicSettings.BrainO2Blackout) / (LogicSettings.BrainO2Full - LogicSettings.BrainO2Blackout),
0.0, 1.0);
- double perfRatio = Math.Clamp(bloodHead / LogicSettings.RestingBloodHead, 0.0, 1.0);
+ double perfRatio = Clamp(bloodHead / LogicSettings.RestingBloodHead, 0.0, 1.0);
perfusionLevel = perfRatio;
// Use soft minimum for consciousness mapping (not blackout threshold)
- double perfNorm = Math.Clamp(
+ double perfNorm = Clamp(
(perfRatio - LogicSettings.ConsciousnessPerfusionSoftMinRatio) /
(1.0 - LogicSettings.ConsciousnessPerfusionSoftMinRatio),
0.0, 1.0);
@@ -297,10 +360,10 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
((LogicSettings.ConsciousnessLossTauMin - LogicSettings.ConsciousnessLossTauMax) * lossSeverity);
// Critical collapse accelerator (mostly affects extreme +G)
- double criticalPerf = 1.0 - Math.Clamp(
+ double criticalPerf = 1.0 - Clamp(
perfNorm / LogicSettings.ConsciousnessCriticalPerfusionNorm, 0.0, 1.0);
- double criticalO2 = 1.0 - Math.Clamp(
+ double criticalO2 = 1.0 - Clamp(
o2Normalized / LogicSettings.ConsciousnessCriticalO2Norm, 0.0, 1.0);
double critical = Math.Max(criticalPerf, criticalO2);
@@ -311,25 +374,25 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
double tau = targetConsciousness < consciousnessLevel ? lossTau : LogicSettings.ConsciousnessRecoveryTau;
consciousnessLevel = StepTowardsLinear(consciousnessLevel, targetConsciousness, tau, dt);
- consciousnessLevel = Math.Clamp(consciousnessLevel, 0.0, 1.0);
+ consciousnessLevel = Clamp(consciousnessLevel, 0.0, 1.0);
// Visual symptoms should start from physiology, but once consciousness is collapsing
// they should continue toward full obscuration even if perfusion briefly rebounds.
- double visualPerf = Math.Clamp((perfRatio - 0.45) / 0.55, 0.0, 1.0);
- double visualO2 = Math.Clamp((o2Normalized - 0.15) / 0.85, 0.0, 1.0);
+ double visualPerf = Clamp((perfRatio - 0.45) / 0.55, 0.0, 1.0);
+ double visualO2 = Clamp((o2Normalized - 0.15) / 0.85, 0.0, 1.0);
// Fast visual reserve from physiology.
double visualReserve = (0.7 * visualPerf) + (0.3 * visualO2);
double visualDeficit = 1.0 - visualReserve;
- // Early/mid visual impairment path.
+ // Early/mid-visual impairment path.
// Keeps onset before LOC, but avoids saturating too early.
- double physiologicalTunnelTarget = Math.Clamp((visualDeficit - 0.18) / 0.82, 0.0, 1.0);
+ double physiologicalTunnelTarget = Clamp((visualDeficit - 0.18) / 0.82, 0.0, 1.0);
physiologicalTunnelTarget = Math.Pow(physiologicalTunnelTarget, 2.2);
// Blackout path: if consciousness gets close to zero, tunnel vision must approach 1.
// This also reduces sensitivity to short perfusion recoveries.
- double blackoutTunnelTarget = Math.Clamp(1.0 - consciousnessLevel, 0.0, 1.0);
+ double blackoutTunnelTarget = Clamp(1.0 - consciousnessLevel, 0.0, 1.0);
blackoutTunnelTarget = Math.Pow(blackoutTunnelTarget, 2);
// Use whichever impairment is worse.
@@ -347,11 +410,11 @@ public virtual void Update(double dt, double gz, double gx = 0.0, double gy = 0.
tunnelVisionLevel = StepTowardsLinear(tunnelVisionLevel, tunnelTarget, tunnelTau, dt);
greyScaleLevel = StepTowardsLinear(greyScaleLevel, greyTarget, greyTau, dt);
- tunnelVisionLevel = Math.Clamp(tunnelVisionLevel, 0.0, 1.0);
- greyScaleLevel = Math.Clamp(greyScaleLevel, 0.0, 1.0);
+ tunnelVisionLevel = Clamp(tunnelVisionLevel, 0.0, 1.0);
+ greyScaleLevel = Clamp(greyScaleLevel, 0.0, 1.0);
// Hard guarantee: only enforce near total visual loss when consciousness is very close to zero.
- double nearLoc = Math.Clamp((0.12 - consciousnessLevel) / 0.12, 0.0, 1.0);
+ double nearLoc = Clamp((0.12 - consciousnessLevel) / 0.12, 0.0, 1.0);
double tunnelFloorFromConsciousness = SmoothStep(nearLoc);
tunnelVisionLevel = Math.Max(tunnelVisionLevel, tunnelFloorFromConsciousness);
diff --git a/GraphicLogicTest/GraphicLogicTest.csproj b/GraphicLogicTest/GraphicLogicTest.csproj
index c30ab76..2c3c8ea 100644
--- a/GraphicLogicTest/GraphicLogicTest.csproj
+++ b/GraphicLogicTest/GraphicLogicTest.csproj
@@ -21,4 +21,15 @@
+
+
+ SimulationView.axaml
+ Code
+
+
+ SecondaryView.axaml
+ Code
+
+
+
diff --git a/GraphicLogicTest/MainWindow.axaml b/GraphicLogicTest/MainWindow.axaml
index 942814b..2e9d8b0 100644
--- a/GraphicLogicTest/MainWindow.axaml
+++ b/GraphicLogicTest/MainWindow.axaml
@@ -1,210 +1,40 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GraphicLogicTest/MainWindow.xaml.cs b/GraphicLogicTest/MainWindow.xaml.cs
index 2977fc6..b144fa6 100644
--- a/GraphicLogicTest/MainWindow.xaml.cs
+++ b/GraphicLogicTest/MainWindow.xaml.cs
@@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
-using Avalonia.Threading;
-using GraphicLogicTest.Logging;
+using GraphicLogicTest.Views.SimulationView;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.Measure;
@@ -14,162 +13,105 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
+using GraphicLogicTest.Logging;
+using GraphicLogicTest.Views.GLoCPlot;
namespace GraphicLogicTest
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
- private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(250) };
- private DateTime _lastTime;
- private bool _globalPaused;
-
- private int _instanceCounter = 1;
-
- public ObservableCollection Instances { get; } = [];
- public ObservableCollection LogicSettingsEntries { get; } = [];
-
- private double _timeMultiplier = 5.0;
- public double TimeMultiplier { get => _timeMultiplier; set { _timeMultiplier = value; OnPropertyChanged(); } }
+ private WindowViewEntry? _activeWindow;
+ public ObservableCollection WindowViews { get; } = [];
+ private readonly SimulationViewModel _simulationViewModel;
- private int _updateMultiplier = 5;
- public int UpdateMultiplier { get => _updateMultiplier; set { _updateMultiplier = value; OnPropertyChanged(); } }
-
- private double _recordedTime = 30.0;
- public double RecordedTime
- {
- get => _recordedTime;
- set
- {
- _recordedTime = value;
- OnPropertyChanged();
- foreach (var vm in Instances) vm.UpdateRecordedTime(value);
- }
- }
+ public Control? ActiveWindowContent => _activeWindow?.Content;
public MainWindow()
{
- InitializeComponent();
- DataContext = this;
-
_ = new TestLogging();
GEffectsLogic.Logging.Logger.Instance = new LogicLogging();
GEffectsLogic.LogicSettings.DebugMode = true;
-
- BuildLogicSettingsEntries();
-
- AddInstanceInternal("[1 5 5],[25]");
- AddInstanceInternal("[1 9 9],[21]");
-
- _lastTime = DateTime.Now;
- _timer.Tick += UpdateGraph;
- _timer.Start();
+
+ _simulationViewModel = new SimulationViewModel();
+
+ InitializeComponent();
+ DataContext = this;
+ RegisterWindowViews();
}
- private void BuildLogicSettingsEntries()
+ private void RegisterWindowViews()
{
- var props = typeof(GEffectsLogic.LogicSettings)
- .GetProperties(BindingFlags.Public | BindingFlags.Static)
- .Where(p => p.CanRead && p.CanWrite);
+ WindowViews.Clear();
- foreach (var prop in props)
- {
- LogicSettingsEntries.Add(new LogicSettingEntry(prop));
- }
- }
+ // Register new windows only here.
+ WindowViews.Add(new WindowViewEntry(0, "Simulation", new SimulationView { DataContext = _simulationViewModel }));
+ WindowViews.Add(new WindowViewEntry(1, "GLoC Plot", new GLoCPlot()));
- private void AddInstanceInternal(string defaultSequence)
- {
- var vm = new SimulationInstanceViewModel($"Instance {_instanceCounter++}", defaultSequence, RecordedTime);
- Instances.Add(vm);
+ SetActiveWindow(0);
}
- private void UpdateGraph(object? sender, EventArgs e)
+ private void SetActiveWindow(int index)
{
- var now = DateTime.Now;
- var dt = (now - _lastTime).TotalSeconds;
- _lastTime = now;
+ if (index < 0 || index >= WindowViews.Count) return;
- var scaledDt = dt * TimeMultiplier;
- var iterations = Math.Max(1, UpdateMultiplier);
- var dtIteration = scaledDt / iterations;
+ var next = WindowViews[index];
+ if (ReferenceEquals(_activeWindow, next)) return;
- for (int i = 0; i < iterations; i++)
- {
- foreach (var vm in Instances)
- {
- if (vm.IsPaused) continue;
- vm.Step(dtIteration, RecordedTime);
- }
- }
- }
+ _activeWindow = next;
- private void GlobalStart_Click(object? sender, RoutedEventArgs e)
- {
- foreach (var vm in Instances)
+ foreach (var item in WindowViews)
{
- if (!vm.HasStarted)
- vm.StartInstance();
- else
- vm.IsPaused = false;
+ item.IsActive = ReferenceEquals(item, _activeWindow);
}
- }
- private void GlobalStop_Click(object? sender, RoutedEventArgs e)
- {
- foreach (var item in Instances)
- {
- item.IsPaused = true;
- }
+ OnPropertyChanged(nameof(ActiveWindowContent));
}
- private void ResetAll_Click(object? sender, RoutedEventArgs e)
+ private void SelectWindow_Click(object? sender, RoutedEventArgs e)
{
- foreach (var vm in Instances)
- {
- vm.ResetModel();
- }
- }
+ if (sender is not Button b) return;
+ if (b.Tag is null) return;
+ if (!int.TryParse(b.Tag.ToString(), out var index)) return;
- private void AddInstance_Click(object? sender, RoutedEventArgs e)
- {
- AddInstanceInternal("[1 5 5],[25]");
+ SetActiveWindow(index);
}
- private void InstanceStart_Click(object? sender, RoutedEventArgs e)
- {
- if (sender is Control c && c.DataContext is SimulationInstanceViewModel vm)
- {
- vm.StartInstance();
- }
- }
+ public new event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
- private void InstancePause_Click(object? sender, RoutedEventArgs e)
- {
- if (sender is Control c && c.DataContext is SimulationInstanceViewModel vm)
- {
- vm.IsPaused = !vm.IsPaused;
- }
- }
+ public sealed class WindowViewEntry : INotifyPropertyChanged
+ {
+ private bool _isActive;
+
+ public int Index { get; }
+ public string Title { get; }
+ public Control Content { get; }
- private void InstanceDelete_Click(object? sender, RoutedEventArgs e)
+ public bool IsActive
{
- if (sender is Control c && c.DataContext is SimulationInstanceViewModel vm)
+ get => _isActive;
+ set
{
- Instances.Remove(vm);
+ if (_isActive == value) return;
+ _isActive = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(ButtonBackground));
}
}
- private void ToggleMetricSeries_Click(object? sender, RoutedEventArgs e)
- {
- if (sender is not Button b || b.DataContext is not SimulationInstanceViewModel vm) return;
- if (b.Tag is null) return;
- if (!int.TryParse(b.Tag.ToString(), out var index)) return;
+ public string ButtonBackground => IsActive ? "#FF2D6FDB" : "#FF606060";
- vm.ToggleMetricSeries(index);
+ public WindowViewEntry(int index, string title, Control content)
+ {
+ Index = index;
+ Title = title;
+ Content = content;
}
- public new event PropertyChangedEventHandler? PropertyChanged;
- protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ public event PropertyChangedEventHandler? PropertyChanged;
+ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
diff --git a/GraphicLogicTest/SimulationViewModel.cs b/GraphicLogicTest/SimulationViewModel.cs
new file mode 100644
index 0000000..ba123d4
--- /dev/null
+++ b/GraphicLogicTest/SimulationViewModel.cs
@@ -0,0 +1,158 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using GraphicLogicTest.Logging;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace GraphicLogicTest
+{
+ public sealed class SimulationViewModel : INotifyPropertyChanged
+ {
+ private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(250) };
+ private DateTime _lastTime;
+ private int _instanceCounter = 1;
+
+ public ObservableCollection Instances { get; } = [];
+ public ObservableCollection LogicSettingsEntries { get; } = [];
+
+ private double _timeMultiplier = 5.0;
+ public double TimeMultiplier { get => _timeMultiplier; set { _timeMultiplier = value; OnPropertyChanged(); } }
+
+ private int _updateMultiplier = 5;
+ public int UpdateMultiplier { get => _updateMultiplier; set { _updateMultiplier = value; OnPropertyChanged(); } }
+
+ private double _recordedTime = 30.0;
+ public double RecordedTime
+ {
+ get => _recordedTime;
+ set
+ {
+ _recordedTime = value;
+ OnPropertyChanged();
+ foreach (var vm in Instances) vm.UpdateRecordedTime(value);
+ }
+ }
+
+ public SimulationViewModel()
+ {
+ BuildLogicSettingsEntries();
+
+ AddInstanceInternal("[1 5 5],[25]");
+ AddInstanceInternal("[1 9 9],[21]");
+
+ _lastTime = DateTime.Now;
+ _timer.Tick += UpdateGraph;
+ _timer.Start();
+ }
+
+ private void BuildLogicSettingsEntries()
+ {
+ var props = typeof(GEffectsLogic.LogicSettings)
+ .GetProperties(BindingFlags.Public | BindingFlags.Static)
+ .Where(p => p.CanRead && p.CanWrite);
+
+ foreach (var prop in props)
+ {
+ LogicSettingsEntries.Add(new LogicSettingEntry(prop));
+ }
+ }
+
+ private void AddInstanceInternal(string defaultSequence)
+ {
+ var vm = new SimulationInstanceViewModel($"Instance {_instanceCounter++}", defaultSequence, RecordedTime);
+ Instances.Add(vm);
+ }
+
+ private void UpdateGraph(object? sender, EventArgs e)
+ {
+ var now = DateTime.Now;
+ var dt = (now - _lastTime).TotalSeconds;
+ _lastTime = now;
+
+ var scaledDt = dt * TimeMultiplier;
+ var iterations = Math.Max(1, UpdateMultiplier);
+ var dtIteration = scaledDt / iterations;
+
+ for (int i = 0; i < iterations; i++)
+ {
+ foreach (var vm in Instances)
+ {
+ if (vm.IsPaused) continue;
+ vm.Step(dtIteration, RecordedTime);
+ }
+ }
+ }
+
+ internal void GlobalStart_Click(object? sender, RoutedEventArgs e)
+ {
+ foreach (var vm in Instances)
+ {
+ if (!vm.HasStarted)
+ vm.StartInstance();
+ else
+ vm.IsPaused = false;
+ }
+ }
+
+ internal void GlobalStop_Click(object? sender, RoutedEventArgs e)
+ {
+ foreach (var item in Instances)
+ {
+ item.IsPaused = true;
+ }
+ }
+
+ internal void ResetAll_Click(object? sender, RoutedEventArgs e)
+ {
+ foreach (var vm in Instances)
+ {
+ vm.ResetModel();
+ }
+ }
+
+ internal void AddInstance_Click(object? sender, RoutedEventArgs e)
+ {
+ AddInstanceInternal("[1 5 5],[25]");
+ }
+
+ internal void InstanceStart_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Control c && c.DataContext is SimulationInstanceViewModel vm)
+ {
+ vm.StartInstance();
+ }
+ }
+
+ internal void InstancePause_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Control c && c.DataContext is SimulationInstanceViewModel vm)
+ {
+ vm.IsPaused = !vm.IsPaused;
+ }
+ }
+
+ internal void InstanceDelete_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is Control c && c.DataContext is SimulationInstanceViewModel vm)
+ {
+ Instances.Remove(vm);
+ }
+ }
+
+ internal void ToggleMetricSeries_Click(object? sender, RoutedEventArgs e)
+ {
+ if (sender is not Button b || b.DataContext is not SimulationInstanceViewModel vm) return;
+ if (b.Tag is null) return;
+ if (!int.TryParse(b.Tag.ToString(), out var index)) return;
+
+ vm.ToggleMetricSeries(index);
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/GraphicLogicTest/Views/GLoCPlot/GLoCPlot.axaml b/GraphicLogicTest/Views/GLoCPlot/GLoCPlot.axaml
new file mode 100644
index 0000000..b0dae9e
--- /dev/null
+++ b/GraphicLogicTest/Views/GLoCPlot/GLoCPlot.axaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GraphicLogicTest/Views/GLoCPlot/GLoCPlot.axaml.cs b/GraphicLogicTest/Views/GLoCPlot/GLoCPlot.axaml.cs
new file mode 100644
index 0000000..be95934
--- /dev/null
+++ b/GraphicLogicTest/Views/GLoCPlot/GLoCPlot.axaml.cs
@@ -0,0 +1,110 @@
+using System.Collections.ObjectModel;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using GEffectsLogic;
+using LiveChartsCore;
+using LiveChartsCore.Defaults;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using SkiaSharp;
+
+namespace GraphicLogicTest.Views.GLoCPlot
+{
+ public partial class GLoCPlot : UserControl
+ {
+ public GEffectsLogicInstance LogicInstance { get; private set; }
+
+ public string StartG { get; set; } = "1";
+ public string EndG { get; set; } = "9";
+ public string Iterations { get; set; } = "10";
+
+ private ObservableCollection GLoCPoints = new();
+
+ public List Series { get; private set; }
+
+ public Axis[] XAxes { get; } = [new Axis { MinLimit = 0 }];
+ public Axis[] YAxes { get; } = [new Axis { MinLimit = 0 }];
+
+ public GLoCPlot()
+ {
+ AvaloniaXamlLoader.Load(this);
+ GLoCPoints.Add(new ObservablePoint(1, 0));
+ GLoCPoints.Add(new ObservablePoint(0.25, 0.5));
+ GLoCPoints.Add(new ObservablePoint(0.0, 1.0));
+
+ Series =
+ [
+ new LineSeries()
+ {
+ Name = "GLoC",
+ Values = GLoCPoints,
+ GeometrySize = 0,
+ LineSmoothness = 0,
+ Stroke = new SolidColorPaint(SKColors.Red),
+ Fill = null
+ },
+ ];
+ DataContext = this;
+
+ LogicInstance = new GEffectsLogicInstance();
+ }
+
+ private void AddGAccelerationLine(double startG, double endG, double endTime)
+ {
+ Series.Add(new LineSeries()
+ {
+ Name = "GAcceleration",
+ Values = new ObservableCollection()
+ {
+ new ObservablePoint(0.0, startG),
+ new ObservablePoint(endTime, endG)
+ },
+ GeometrySize = 0,
+ Stroke = new SolidColorPaint(SKColors.DarkViolet),
+ Fill = null
+ });
+ }
+
+ private void OnStartClicked(object? sender, RoutedEventArgs e)
+ {
+ GLoCPoints.Clear();
+ Series.Clear();
+ Series.Add(
+ new LineSeries()
+ {
+ Name = "GLoC",
+ Values = GLoCPoints,
+ GeometrySize = 0,
+ LineSmoothness = 0,
+ Stroke = new SolidColorPaint(SKColors.Red),
+ Fill = null
+ }
+ );
+
+ if (
+ double.TryParse(StartG, out var startG) &&
+ double.TryParse(EndG, out var endG) &&
+ int.TryParse(Iterations, out var iterations)
+ )
+ {
+ double stepSize = (endG - startG) / (iterations - 1);
+ for (double i = startG; i <= endG; i += stepSize)
+ {
+ // run LogicInstance with a constant acceleration of i until GLoC
+ double currentG = 1.0;
+ double dT = 0.2;
+ LogicInstance.Reset();
+ while (LogicInstance.ConsciousnessLevel >= 0.05)
+ {
+ LogicInstance.Update(dT, 0.0, 0.0, currentG);
+ currentG += i * dT;
+ }
+
+ GLoCPoints.Add(new ObservablePoint(LogicInstance.Time, currentG));
+ AddGAccelerationLine(startG, currentG, LogicInstance.Time);
+ }
+ }
+ }
+ }
+}
diff --git a/GraphicLogicTest/Views/SimulationView/SimulationView.axaml b/GraphicLogicTest/Views/SimulationView/SimulationView.axaml
new file mode 100644
index 0000000..a964068
--- /dev/null
+++ b/GraphicLogicTest/Views/SimulationView/SimulationView.axaml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GraphicLogicTest/Views/SimulationView/SimulationView.axaml.cs b/GraphicLogicTest/Views/SimulationView/SimulationView.axaml.cs
new file mode 100644
index 0000000..fa307b6
--- /dev/null
+++ b/GraphicLogicTest/Views/SimulationView/SimulationView.axaml.cs
@@ -0,0 +1,26 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace GraphicLogicTest.Views.SimulationView
+{
+ public partial class SimulationView : UserControl
+ {
+ public SimulationView()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private SimulationViewModel? GetSimulationViewModel() => DataContext as SimulationViewModel;
+
+ private void GlobalStart_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.GlobalStart_Click(sender, e);
+ private void GlobalStop_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.GlobalStop_Click(sender, e);
+ private void ResetAll_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.ResetAll_Click(sender, e);
+ private void AddInstance_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.AddInstance_Click(sender, e);
+
+ private void InstanceStart_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.InstanceStart_Click(sender, e);
+ private void InstancePause_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.InstancePause_Click(sender, e);
+ private void InstanceDelete_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.InstanceDelete_Click(sender, e);
+ private void ToggleMetricSeries_Click(object? sender, RoutedEventArgs e) => GetSimulationViewModel()?.ToggleMetricSeries_Click(sender, e);
+ }
+}