Medical Vitals - Add SPO2 (#9360)

Co-authored-by: johnb432 <58661205+johnb432@users.noreply.github.com>
Co-authored-by: Grim <69561145+LinkIsGrim@users.noreply.github.com>
Co-authored-by: LinkIsGrim <salluci.lovi@gmail.com>
This commit is contained in:
BrettMayson 2024-02-07 14:50:18 -06:00 committed by GitHub
parent 2f9b7002c3
commit 1649422cbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 206 additions and 11 deletions

View File

@ -60,7 +60,7 @@ if (!hasInterface) exitWith {};
}, true] call CBA_fnc_addPlayerEventHandler;
// - Duty factors -------------------------------------------------------------
if (["ace_medical"] call EFUNC(common,isModLoaded)) then {
if (GVAR(medicalLoaded)) then {
[QEGVAR(medical,pain), { // 0->1.0, 0.5->1.05, 1->1.1
linearConversion [0, 1, (_this getVariable [QEGVAR(medical,pain), 0]), 1, 1.1, true];
}] call FUNC(addDutyFactor);

View File

@ -13,5 +13,6 @@ GVAR(dutyList) = createHashMap;
GVAR(setAnimExclusions) = [];
GVAR(inertia) = 0;
GVAR(inertiaCache) = createHashMap;
GVAR(medicalLoaded) = ["ace_medical"] call EFUNC(common,isModLoaded);
ADDON = true;

View File

@ -23,6 +23,12 @@ if (!alive ACE_player) exitWith {
_staminaBarContainer ctrlCommit 1;
};
private _oxygen = 0.9; // Default AF oxygen saturation
if (GVAR(medicalLoaded) && {EGVAR(medical_vitals,simulateSpo2)}) then {
_oxygen = (ACE_player getVariable [QEGVAR(medical,spo2), 97]) / 100;
};
private _currentWork = REE;
private _currentSpeed = (vectorMagnitude (velocity ACE_player)) min 6;
@ -42,8 +48,8 @@ GVAR(muscleDamage) = (GVAR(muscleDamage) + (_currentWork / GVAR(peakPower)) ^ 3.
private _muscleIntegritySqrt = sqrt (1 - GVAR(muscleDamage));
// Calculate available power
private _ae1PathwayPowerFatigued = GVAR(ae1PathwayPower) * sqrt (GVAR(ae1Reserve) / AE1_MAXRESERVE) * OXYGEN * _muscleIntegritySqrt;
private _ae2PathwayPowerFatigued = GVAR(ae2PathwayPower) * sqrt (GVAR(ae2Reserve) / AE2_MAXRESERVE) * OXYGEN * _muscleIntegritySqrt;
private _ae1PathwayPowerFatigued = GVAR(ae1PathwayPower) * sqrt (GVAR(ae1Reserve) / AE1_MAXRESERVE) * _oxygen * _muscleIntegritySqrt;
private _ae2PathwayPowerFatigued = GVAR(ae2PathwayPower) * sqrt (GVAR(ae2Reserve) / AE2_MAXRESERVE) * _oxygen * _muscleIntegritySqrt;
// Calculate how much power is consumed from each reserve
private _ae1Power = _currentWork min _ae1PathwayPowerFatigued;
@ -58,8 +64,8 @@ GVAR(anReserve) = GVAR(anReserve) - _anPower / WATTSPERATP;
GVAR(anFatigue) = GVAR(anFatigue) + _anPower * (0.057 / GVAR(peakPower)) * 1.1;
// Aerobic ATP reserve recovery
GVAR(ae1Reserve) = ((GVAR(ae1Reserve) + OXYGEN * 6.60 * (GVAR(ae1PathwayPower) - _ae1Power) / GVAR(ae1PathwayPower) * GVAR(recoveryFactor)) min AE1_MAXRESERVE) max 0;
GVAR(ae2Reserve) = ((GVAR(ae2Reserve) + OXYGEN * 5.83 * (GVAR(ae2PathwayPower) - _ae2Power) / GVAR(ae2PathwayPower) * GVAR(recoveryFactor)) min AE2_MAXRESERVE) max 0;
GVAR(ae1Reserve) = ((GVAR(ae1Reserve) + _oxygen * 6.60 * (GVAR(ae1PathwayPower) - _ae1Power) / GVAR(ae1PathwayPower) * GVAR(recoveryFactor)) min AE1_MAXRESERVE) max 0;
GVAR(ae2Reserve) = ((GVAR(ae2Reserve) + _oxygen * 5.83 * (GVAR(ae2PathwayPower) - _ae2Power) / GVAR(ae2PathwayPower) * GVAR(recoveryFactor)) min AE2_MAXRESERVE) max 0;
// Anaerobic ATP reserver and fatigue recovery
GVAR(anReserve) = ((GVAR(anReserve)
@ -70,9 +76,9 @@ GVAR(anFatigue) = ((GVAR(anFatigue)
- (_ae1PathwayPowerFatigued + _ae2PathwayPowerFatigued - _ae1Power - _ae2Power) * (0.057 / GVAR(peakPower)) * GVAR(anFatigue) ^ 2 * GVAR(recoveryFactor)
) min 1) max 0;
private _aeReservePercentage = (GVAR(ae1Reserve) / AE1_MAXRESERVE + GVAR(ae2Reserve) / AE2_MAXRESERVE) / 2;
private _anReservePercentage = GVAR(anReserve) / AN_MAXRESERVE;
private _perceivedFatigue = 1 - (_anReservePercentage min _aeReservePercentage);
GVAR(aeReservePercentage) = (GVAR(ae1Reserve) / AE1_MAXRESERVE + GVAR(ae2Reserve) / AE2_MAXRESERVE) / 2;
GVAR(anReservePercentage) = GVAR(anReserve) / AN_MAXRESERVE;
private _perceivedFatigue = 1 - (GVAR(anReservePercentage) min GVAR(aeReservePercentage));
[ACE_player, _perceivedFatigue, _currentSpeed, GVAR(anReserve) == 0] call FUNC(handleEffects);

View File

@ -42,6 +42,7 @@
#define GET_ARRAY(config,default) (if (isArray (config)) then {getArray (config)} else {default})
#define DEFAULT_HEART_RATE 80
#define DEFAULT_SPO2 97
#define DEFAULT_PERIPH_RES 100
// --- blood
@ -153,6 +154,8 @@
#define VAR_WOUND_BLEEDING QEGVAR(medical,woundBleeding)
#define VAR_CRDC_ARRST QEGVAR(medical,inCardiacArrest)
#define VAR_HEART_RATE QEGVAR(medical,heartRate)
#define VAR_SPO2 QEGVAR(medical,spo2)
#define VAR_OXYGEN_DEMAND QEGVAR(medical,oxygenDemand)
#define VAR_PAIN QEGVAR(medical,pain)
#define VAR_PAIN_SUPP QEGVAR(medical,painSuppress)
#define VAR_PERIPH_RES QEGVAR(medical,peripheralResistance)
@ -175,6 +178,7 @@
#define GET_BLOOD_VOLUME(unit) (unit getVariable [VAR_BLOOD_VOL, DEFAULT_BLOOD_VOLUME])
#define GET_WOUND_BLEEDING(unit) (unit getVariable [VAR_WOUND_BLEEDING, 0])
#define GET_HEART_RATE(unit) (unit getVariable [VAR_HEART_RATE, DEFAULT_HEART_RATE])
#define GET_SPO2(unit) (unit getVariable [VAR_SPO2, DEFAULT_SPO2])
#define GET_HEMORRHAGE(unit) (unit getVariable [VAR_HEMORRHAGE, 0])
#define GET_PAIN(unit) (unit getVariable [VAR_PAIN, 0])
#define GET_PAIN_SUPPRESS(unit) (unit getVariable [VAR_PAIN_SUPP, 0])

View File

@ -32,13 +32,15 @@ if (damage _unit > 0) then {
if (_isRespawn) then {
TRACE_1("reseting all vars on respawn",_isRespawn); // note: state is handled by ace_medical_statemachine_fnc_resetStateDefault
// - Blood and heart ----------------------------------------------------------
// - Vitals ------------------------------------------------------------------
_unit setVariable [VAR_BLOOD_VOL, DEFAULT_BLOOD_VOLUME, true];
_unit setVariable [VAR_HEART_RATE, DEFAULT_HEART_RATE, true];
_unit setVariable [VAR_BLOOD_PRESS, [80, 120], true];
_unit setVariable [VAR_PERIPH_RES, DEFAULT_PERIPH_RES, true];
_unit setVariable [VAR_CRDC_ARRST, false, true];
_unit setVariable [VAR_HEMORRHAGE, 0, true];
_unit setVariable [VAR_SPO2, DEFAULT_SPO2, true];
_unit setVariable [VAR_OXYGEN_DEMAND, 0, true];
// - Pain ---------------------------------------------------------------------
_unit setVariable [VAR_PAIN, 0, true];

View File

@ -63,6 +63,8 @@ _patient setVariable [VAR_FRACTURES, DEFAULT_FRACTURE_VALUES, true];
_patient setVariable [VAR_HEART_RATE, DEFAULT_HEART_RATE, true];
_patient setVariable [VAR_BLOOD_PRESS, [80, 120], true];
_patient setVariable [VAR_PERIPH_RES, DEFAULT_PERIPH_RES, true];
_patient setVariable [VAR_SPO2, DEFAULT_SPO2, true];
_patient setVariable [VAR_OXYGEN_DEMAND, 0, true];
// IVs
_patient setVariable [QEGVAR(medical,ivBags), nil, true];

View File

@ -0,0 +1,10 @@
class CfgWeapons {
class H_HelmetB;
class H_PilotHelmetFighter_B: H_HelmetB {
GVAR(oxygenSupply) = QUOTE(vehicle _this isKindOf 'Plane' || vehicle _this isKindOf 'Helicopter');
};
class Vest_Camo_Base;
class V_RebreatherB: Vest_Camo_Base {
GVAR(oxygenSupply) = QUOTE(eyePos _this select 2 < 0); // will only work for sea-level water
};
};

View File

@ -1,4 +1,6 @@
PREP(handleUnitVitals);
PREP(scanConfig);
PREP(updateHeartRate);
PREP(updateOxygen);
PREP(updatePainSuppress);
PREP(updatePeripheralResistance);

View File

@ -6,4 +6,8 @@ PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
#include "initSettings.inc.sqf"
GVAR(oxygenSupplyConditionCache) = uiNamespace getVariable QGVAR(oxygenSupplyConditionCache);
ADDON = true;

View File

@ -1,3 +1,9 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"
GVAR(oxygenSupplyConditionCache) = createHashMap;
call FUNC(scanConfig);
GVAR(oxygenSupplyConditionCache) = compileFinal GVAR(oxygenSupplyConditionCache);

View File

@ -23,5 +23,6 @@ class CfgPatches {
};
#include "CfgEventHandlers.hpp"
#include "CfgWeapons.hpp"
#endif

View File

@ -31,6 +31,9 @@ if (_syncValues) then {
_unit setVariable [QGVAR(lastMomentValuesSynced), CBA_missionTime];
};
// Update SPO2 intake and usage since last update
[_unit, _deltaT, _syncValues] call FUNC(updateOxygen);
private _bloodVolume = GET_BLOOD_VOLUME(_unit) + ([_unit, _deltaT, _syncValues] call EFUNC(medical_status,getBloodVolumeChange));
_bloodVolume = 0 max _bloodVolume min DEFAULT_BLOOD_VOLUME;

View File

@ -0,0 +1,23 @@
#include "..\script_component.hpp"
/*
* Author: LinkIsGrim
* Cache a hashmap of all oxygen-providing items for SpO2 simulation
*
* Arguments:
* None
*
* Return Value:
* None
*
* Public: No
*/
private _filter = toString {getText (_x >> QGVAR(oxygenSupply)) != ""};
{
private _cfgRoot = configFile >> _x;
{
private _condition = compile getText (_x >> QGVAR(oxygenSupply));
GVAR(oxygenSupplyConditionCache) set [configName _x, _condition];
} forEach (_filter configClasses _cfgRoot);
} forEach ["CfgWeapons", "CfgGoggles"];

View File

@ -37,6 +37,7 @@ if IN_CRDC_ARRST(_unit) then {
if (_bloodVolume > BLOOD_VOLUME_CLASS_4_HEMORRHAGE) then {
GET_BLOOD_PRESSURE(_unit) params ["_bloodPressureL", "_bloodPressureH"];
private _meanBP = (2/3) * _bloodPressureH + (1/3) * _bloodPressureL;
private _spo2 = GET_SPO2(_unit);
private _painLevel = GET_PAIN_PERCEIVED(_unit);
private _targetBP = 107;
@ -51,8 +52,11 @@ if IN_CRDC_ARRST(_unit) then {
if (_painLevel > 0.2) then {
_targetHR = _targetHR max (80 + 50 * _painLevel);
};
// Increase HR to compensate for low blood oxygen
// Increase HR to compensate for higher oxygen demand (e.g. running, recovering from sprint)
private _oxygenDemand = _unit getVariable [VAR_OXYGEN_DEMAND, 0];
_targetHR = _targetHR + ((97 - _spo2) * 2) + (_oxygenDemand * -1000);
_targetHR = (_targetHR + _hrTargetAdjustment) max 0;
_hrChange = round(_targetHR - _heartRate) / 2;
} else {
_hrChange = -round(_heartRate / 10);

View File

@ -0,0 +1,75 @@
#include "..\script_component.hpp"
/*
* Author: Brett Mayson
* Update the oxygen levels
*
* Arguments:
* 0: The Unit <OBJECT>
* 1: Time since last update <NUMBER>
* 2: Sync value? <BOOL>
*
* ReturnValue:
* Current SPO2 <NUMBER>
*
* Example:
* [player, 1, false] call ace_medical_vitals_fnc_updateOxygen
*
* Public: No
*/
params ["_unit", "_deltaT", "_syncValue"];
if (!GVAR(simulateSpO2)) exitWith {}; // changing back to default is handled in initSettings.inc.sqf
#define IDEAL_PPO2 0.255
private _current = GET_SPO2(_unit);
private _heartRate = GET_HEART_RATE(_unit);
private _altitude = EGVAR(common,mapAltitude) + ((getPosASL _unit) select 2);
private _po2 = if (missionNamespace getVariable [QEGVAR(weather,enabled), false]) then {
private _temperature = _altitude call EFUNC(weather,calculateTemperatureAtHeight);
private _pressure = _altitude call EFUNC(weather,calculateBarometricPressure);
[_temperature, _pressure, EGVAR(weather,currentHumidity)] call EFUNC(weather,calculateOxygenDensity)
} else {
// Rough approximation of the partial pressure of oxygen in the air
0.25725 * (_altitude / 1000 + 1)
};
private _oxygenSaturation = (IDEAL_PPO2 min _po2) / IDEAL_PPO2;
// Check gear for oxygen supply
[goggles _unit, headgear _unit, vest _unit] findIf {
_x in GVAR(oxygenSupplyConditionCache) &&
{ACE_player call (GVAR(oxygenSupplyConditionCache) get _x)} &&
{ // Will only run this if other conditions are met due to lazy eval
_oxygenSaturation = 1;
_po2 = IDEAL_PPO2;
true
}
};
// Base oxygen consumption rate
private _negativeChange = BASE_OXYGEN_USE;
// Fatigue & exercise will demand more oxygen
// Assuming a trained male in midst of peak exercise will have a peak heart rate of ~180 BPM
// Ref: https://academic.oup.com/bjaed/article-pdf/4/6/185/894114/mkh050.pdf table 2, though we don't take stroke volume change into account
if (_unit == ACE_player && {missionNamespace getVariable [QEGVAR(advanced_fatigue,enabled), false]}) then {
_negativeChange = _negativeChange - ((1 - EGVAR(advanced_fatigue,aeReservePercentage)) * 0.1) - ((1 - EGVAR(advanced_fatigue,anReservePercentage)) * 0.05);
};
// Effectiveness of capturing oxygen
// increases slightly as po2 starts lowering
// but falls off quickly as po2 drops further
private _capture = 1 max ((_po2 / IDEAL_PPO2) ^ (-_po2 * 3));
private _positiveChange = _heartRate * 0.00368 * _oxygenSaturation * _capture;
private _breathingEffectiveness = 1;
private _rateOfChange = _negativeChange + (_positiveChange * _breathingEffectiveness);
private _spo2 = (_current + (_rateOfChange * _deltaT)) max 0 min 100;
_unit setVariable [VAR_OXYGEN_DEMAND, _negativeChange - BASE_OXYGEN_USE];
_unit setVariable [VAR_SPO2, _spo2, _syncValue];

View File

@ -0,0 +1,15 @@
[
QGVAR(simulateSpO2),
"CHECKBOX",
[LSTRING(simulateSpO2_DisplayName), LSTRING(simulateSpO2_Description)],
[ELSTRING(medical,Category), LSTRING(SubCategory)],
true,
1,
{
if (_this) exitWith {}; // skip if true
{
_x setVariable [VAR_OXYGEN_DEMAND, 0, true];
_x setVariable [VAR_SPO2, DEFAULT_SPO2, true];
} forEach (allUnits select {local _x})
} // reset oxygen demand on setting change
] call CBA_fnc_addSetting;

View File

@ -16,3 +16,5 @@
#include "\z\ace\addons\medical_engine\script_macros_medical.hpp"
#include "\z\ace\addons\main\script_macros.hpp"
#define BASE_OXYGEN_USE -0.25

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project name="ACE">
<Package name="Medical_Vitals">
<Key ID="STR_ACE_Medical_Vitals_SubCategory">
<English>Vitals</English>
<Portuguese>Vitais</Portuguese>
</Key>
<Key ID="STR_ACE_Medical_Vitals_simulateSpO2_DisplayName">
<English>Enable SpO2 Simulation</English>
</Key>
<Key ID="STR_ACE_Medical_Vitals_simulateSpO2_Description">
<English>Enables oxygen saturation simulation, providing variable heart rate and oxygen demand based on physical activity and altitude. Required for Airway Management.</English>
</Key>
</Package>
</Project>

View File

@ -1,9 +1,9 @@
PREP(calculateAirDensity);
PREP(calculateBarometricPressure);
PREP(calculateDensityAltitude);
PREP(calculateDewPoint);
PREP(calculateHeatIndex);
PREP(calculateOxygenDensity);
PREP(calculateRoughnessLength);
PREP(calculateSpeedOfSound);
PREP(calculateTemperatureAtHeight);

View File

@ -0,0 +1,20 @@
#include "..\script_component.hpp"
/*
* Author: Brett Mayson
* Calculates the oxygen density
*
* Arguments:
* 0: Temperature - °C <NUMBER>
* 1: Pressure - hPa <NUMBER>
* 2: Relative humidity - value between 0.0 and 1.0 <NUMBER>
*
* Return Value:
* Density of oxygen - kg * m^(-3) <NUMBER>
*
* Example:
* [0, 1020] call ace_weather_fnc_calculateOxygenDensity
*
* Public: No
*/
(_this call FUNC(calculateAirDensity)) * 0.21