diff --git a/addons/advanced_fatigue/XEH_postInit.sqf b/addons/advanced_fatigue/XEH_postInit.sqf index 57b57dcf22d..db4a9d9c5e1 100644 --- a/addons/advanced_fatigue/XEH_postInit.sqf +++ b/addons/advanced_fatigue/XEH_postInit.sqf @@ -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); diff --git a/addons/advanced_fatigue/XEH_preInit.sqf b/addons/advanced_fatigue/XEH_preInit.sqf index 643b7b0be00..9f58e44fdf2 100644 --- a/addons/advanced_fatigue/XEH_preInit.sqf +++ b/addons/advanced_fatigue/XEH_preInit.sqf @@ -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; diff --git a/addons/advanced_fatigue/functions/fnc_mainLoop.sqf b/addons/advanced_fatigue/functions/fnc_mainLoop.sqf index 8903bab4959..da469b6d218 100644 --- a/addons/advanced_fatigue/functions/fnc_mainLoop.sqf +++ b/addons/advanced_fatigue/functions/fnc_mainLoop.sqf @@ -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); diff --git a/addons/medical_engine/script_macros_medical.hpp b/addons/medical_engine/script_macros_medical.hpp index 56c1eec4016..5fecd36413a 100644 --- a/addons/medical_engine/script_macros_medical.hpp +++ b/addons/medical_engine/script_macros_medical.hpp @@ -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]) diff --git a/addons/medical_status/functions/fnc_initUnit.sqf b/addons/medical_status/functions/fnc_initUnit.sqf index 786f2be9500..278163d90b9 100644 --- a/addons/medical_status/functions/fnc_initUnit.sqf +++ b/addons/medical_status/functions/fnc_initUnit.sqf @@ -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]; diff --git a/addons/medical_treatment/functions/fnc_fullHealLocal.sqf b/addons/medical_treatment/functions/fnc_fullHealLocal.sqf index e845e043042..7772db0a6ba 100644 --- a/addons/medical_treatment/functions/fnc_fullHealLocal.sqf +++ b/addons/medical_treatment/functions/fnc_fullHealLocal.sqf @@ -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]; diff --git a/addons/medical_vitals/CfgWeapons.hpp b/addons/medical_vitals/CfgWeapons.hpp new file mode 100644 index 00000000000..5c6e4c9fd8e --- /dev/null +++ b/addons/medical_vitals/CfgWeapons.hpp @@ -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 + }; +}; diff --git a/addons/medical_vitals/XEH_PREP.hpp b/addons/medical_vitals/XEH_PREP.hpp index 8f8c4277518..02f1cc5f899 100644 --- a/addons/medical_vitals/XEH_PREP.hpp +++ b/addons/medical_vitals/XEH_PREP.hpp @@ -1,4 +1,6 @@ PREP(handleUnitVitals); +PREP(scanConfig); PREP(updateHeartRate); +PREP(updateOxygen); PREP(updatePainSuppress); PREP(updatePeripheralResistance); diff --git a/addons/medical_vitals/XEH_preInit.sqf b/addons/medical_vitals/XEH_preInit.sqf index b47cf6628db..8cc49805e92 100644 --- a/addons/medical_vitals/XEH_preInit.sqf +++ b/addons/medical_vitals/XEH_preInit.sqf @@ -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; diff --git a/addons/medical_vitals/XEH_preStart.sqf b/addons/medical_vitals/XEH_preStart.sqf index 022888575ed..78dd8ad0e3f 100644 --- a/addons/medical_vitals/XEH_preStart.sqf +++ b/addons/medical_vitals/XEH_preStart.sqf @@ -1,3 +1,9 @@ #include "script_component.hpp" #include "XEH_PREP.hpp" + +GVAR(oxygenSupplyConditionCache) = createHashMap; + +call FUNC(scanConfig); + +GVAR(oxygenSupplyConditionCache) = compileFinal GVAR(oxygenSupplyConditionCache); diff --git a/addons/medical_vitals/config.cpp b/addons/medical_vitals/config.cpp index d105aa31441..290c25bc9b0 100644 --- a/addons/medical_vitals/config.cpp +++ b/addons/medical_vitals/config.cpp @@ -23,5 +23,6 @@ class CfgPatches { }; #include "CfgEventHandlers.hpp" +#include "CfgWeapons.hpp" #endif diff --git a/addons/medical_vitals/functions/fnc_handleUnitVitals.sqf b/addons/medical_vitals/functions/fnc_handleUnitVitals.sqf index 683cfd34b77..c284b00701d 100644 --- a/addons/medical_vitals/functions/fnc_handleUnitVitals.sqf +++ b/addons/medical_vitals/functions/fnc_handleUnitVitals.sqf @@ -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; diff --git a/addons/medical_vitals/functions/fnc_scanConfig.sqf b/addons/medical_vitals/functions/fnc_scanConfig.sqf new file mode 100644 index 00000000000..377b2353152 --- /dev/null +++ b/addons/medical_vitals/functions/fnc_scanConfig.sqf @@ -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"]; diff --git a/addons/medical_vitals/functions/fnc_updateHeartRate.sqf b/addons/medical_vitals/functions/fnc_updateHeartRate.sqf index 9ffc52d6635..187a8ffe58e 100644 --- a/addons/medical_vitals/functions/fnc_updateHeartRate.sqf +++ b/addons/medical_vitals/functions/fnc_updateHeartRate.sqf @@ -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); diff --git a/addons/medical_vitals/functions/fnc_updateOxygen.sqf b/addons/medical_vitals/functions/fnc_updateOxygen.sqf new file mode 100644 index 00000000000..f2c0f68f715 --- /dev/null +++ b/addons/medical_vitals/functions/fnc_updateOxygen.sqf @@ -0,0 +1,75 @@ +#include "..\script_component.hpp" +/* + * Author: Brett Mayson + * Update the oxygen levels + * + * Arguments: + * 0: The Unit + * 1: Time since last update + * 2: Sync value? + * + * ReturnValue: + * Current SPO2 + * + * 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]; diff --git a/addons/medical_vitals/initSettings.inc.sqf b/addons/medical_vitals/initSettings.inc.sqf new file mode 100644 index 00000000000..db762d2c522 --- /dev/null +++ b/addons/medical_vitals/initSettings.inc.sqf @@ -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; diff --git a/addons/medical_vitals/script_component.hpp b/addons/medical_vitals/script_component.hpp index 3bfb4bcc269..1064ebc52cc 100644 --- a/addons/medical_vitals/script_component.hpp +++ b/addons/medical_vitals/script_component.hpp @@ -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 diff --git a/addons/medical_vitals/stringtable.xml b/addons/medical_vitals/stringtable.xml new file mode 100644 index 00000000000..f1d7f3fa5bc --- /dev/null +++ b/addons/medical_vitals/stringtable.xml @@ -0,0 +1,15 @@ + + + + + Vitals + Vitais + + + Enable SpO2 Simulation + + + Enables oxygen saturation simulation, providing variable heart rate and oxygen demand based on physical activity and altitude. Required for Airway Management. + + + diff --git a/addons/weather/XEH_PREP.hpp b/addons/weather/XEH_PREP.hpp index 5f17905849b..220a18ef3c5 100644 --- a/addons/weather/XEH_PREP.hpp +++ b/addons/weather/XEH_PREP.hpp @@ -1,9 +1,9 @@ - PREP(calculateAirDensity); PREP(calculateBarometricPressure); PREP(calculateDensityAltitude); PREP(calculateDewPoint); PREP(calculateHeatIndex); +PREP(calculateOxygenDensity); PREP(calculateRoughnessLength); PREP(calculateSpeedOfSound); PREP(calculateTemperatureAtHeight); diff --git a/addons/weather/functions/fnc_calculateOxygenDensity.sqf b/addons/weather/functions/fnc_calculateOxygenDensity.sqf new file mode 100644 index 00000000000..542b4b2f108 --- /dev/null +++ b/addons/weather/functions/fnc_calculateOxygenDensity.sqf @@ -0,0 +1,20 @@ +#include "..\script_component.hpp" +/* + * Author: Brett Mayson + * Calculates the oxygen density + * + * Arguments: + * 0: Temperature - °C + * 1: Pressure - hPa + * 2: Relative humidity - value between 0.0 and 1.0 + * + * Return Value: + * Density of oxygen - kg * m^(-3) + * + * Example: + * [0, 1020] call ace_weather_fnc_calculateOxygenDensity + * + * Public: No + */ + +(_this call FUNC(calculateAirDensity)) * 0.21