diff --git a/addons/medical_ai/$PBOPREFIX$ b/addons/medical_ai/$PBOPREFIX$ new file mode 100644 index 0000000000..a1074140ea --- /dev/null +++ b/addons/medical_ai/$PBOPREFIX$ @@ -0,0 +1 @@ +z\ace\addons\medical_ai diff --git a/addons/medical_ai/CfgEventHandlers.hpp b/addons/medical_ai/CfgEventHandlers.hpp new file mode 100644 index 0000000000..0d3301d6e0 --- /dev/null +++ b/addons/medical_ai/CfgEventHandlers.hpp @@ -0,0 +1,17 @@ +class Extended_PreStart_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_FILE(XEH_preStart)); + }; +}; + +class Extended_PreInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_FILE(XEH_preInit)); + }; +}; + +class Extended_PostInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_FILE(XEH_postInit)); + }; +}; diff --git a/addons/medical_ai/README.md b/addons/medical_ai/README.md new file mode 100644 index 0000000000..db0073b208 --- /dev/null +++ b/addons/medical_ai/README.md @@ -0,0 +1,10 @@ +ace_medical_ai +========== + +Makes AI units heal themselves and each other. + +## Maintainers + +The people responsible for merging changes to this component or answering potential questions. + +- [BaerMitUmlaut](https://github.com/BaerMitUmlaut) diff --git a/addons/medical_ai/StateMachine.hpp b/addons/medical_ai/StateMachine.hpp new file mode 100644 index 0000000000..36642fbab3 --- /dev/null +++ b/addons/medical_ai/StateMachine.hpp @@ -0,0 +1,86 @@ +class GVAR(stateMachine) { + list = "allUnits select {local _x}"; + skipNull = 1; + + class Initial { + class Injured { + targetState = "Injured"; + condition = QUOTE(call FUNC(isInjured)); + }; + class HealUnit { + targetState = "HealUnit"; + condition = QUOTE((call FUNC(isSafe)) && {call FUNC(wasRequested)}); + }; + }; + + class Injured { + #ifdef DEBUG_MODE_FULL + onState = "systemChat format [""%1 is injured"", _this]"; + #endif + + class InSafety { + targetState = "Safe"; + condition = QUOTE(call FUNC(isSafe)); + }; + }; + + class Safe { + #ifdef DEBUG_MODE_FULL + onState = "systemChat format [""%1 is injured, but safe"", _this]"; + #endif + + class RequestMedic { + targetState = "HealSelf"; + condition = QUOTE(call FUNC(canRequestMedic)); + onTransition = QUOTE(call FUNC(requestMedic)); + }; + class HealSelf { + targetState = "HealSelf"; + condition = "true"; + }; + }; + + class HealSelf { + onState = QUOTE(call FUNC(healSelf)); + onStateLeaving = QUOTE(_this setVariable [ARR_2(QUOTE(QGVAR(treatmentOverAt)),nil)]); + + class Initial { + // Go back to initial state when done healing + targetState = "Initial"; + condition = QUOTE( \ + !(call FUNC(isInjured)) \ + && {_this getVariable [ARR_2(QUOTE(QGVAR(treatmentOverAt)),CBA_missionTime)] <= CBA_missionTime} \ + ); + }; + class Injured { + // Stop treating when it's no more safe + targetState = "Injured"; + condition = QUOTE( \ + !(call FUNC(isSafe)) \ + && {_this getVariable [ARR_2(QUOTE(QGVAR(treatmentOverAt)),CBA_missionTime)] <= CBA_missionTime} \ + ); + }; + }; + + class HealUnit { + onState = QUOTE(call FUNC(healUnit)); + onStateLeaving = QUOTE(_this setVariable [ARR_2(QUOTE(QGVAR(treatmentOverAt)),nil)]); + + class Initial { + // Go back to initial state when done healing or it's no more safe to treat + targetState = "Initial"; + condition = QUOTE( \ + !((call FUNC(wasRequested)) && {call FUNC(isSafe)}) \ + && {_this getVariable [ARR_2(QUOTE(QGVAR(treatmentOverAt)),CBA_missionTime)] <= CBA_missionTime} \ + ); + }; + class Injured { + // Treating yourself has priority + targetState = "Injured"; + condition = QUOTE( \ + (call FUNC(isInjured)) \ + && {_this getVariable [ARR_2(QUOTE(QGVAR(treatmentOverAt)),CBA_missionTime)] <= CBA_missionTime} \ + ); + }; + }; +}; diff --git a/addons/medical_ai/XEH_PREP.hpp b/addons/medical_ai/XEH_PREP.hpp new file mode 100644 index 0000000000..f55636612a --- /dev/null +++ b/addons/medical_ai/XEH_PREP.hpp @@ -0,0 +1,8 @@ +PREP(canRequestMedic); +PREP(healSelf); +PREP(healUnit); +PREP(isInjured); +PREP(isSafe); +PREP(playTreatmentAnim); +PREP(requestMedic); +PREP(wasRequested); diff --git a/addons/medical_ai/XEH_postInit.sqf b/addons/medical_ai/XEH_postInit.sqf new file mode 100644 index 0000000000..4133cc1e04 --- /dev/null +++ b/addons/medical_ai/XEH_postInit.sqf @@ -0,0 +1,31 @@ +#include "script_component.hpp" + +["ace_settingsInitialized", { + // Only run for AI that does not have to deal with advanced medical + if (EGVAR(medical,enableFor) == 1 || {hasInterface && {EGVAR(medical,level) == 2}}) exitWith {}; + + ["ace_firedNonPlayer", { + _unit setVariable [QGVAR(lastFired), CBA_missionTime]; + }] call CBA_fnc_addEventHandler; + + if (hasInterface) then { + ["ace_unconscious", { + params ["_unit", "_unconscious"]; + if (!_unconscious || {_unit != ACE_player}) exitWith {}; + + private _medic = objNull; + { + if ((!isPlayer _x) && {[_x] call EFUNC(medical,isMedic)}) exitWith { + _medic = _x; + }; + } forEach (units _unit); + if (isNull _medic) exitWith {}; + + private _healQueue = _medic getVariable [QGVAR(healQueue), []]; + _healQueue pushBack _unit; + _medic setVariable [QGVAR(healQueue), _healQueue]; + }] call CBA_fnc_addEventHandler; + }; + + GVAR(statemachine) = [configFile >> "ACE_Medical_AI_StateMachine"] call CBA_statemachine_fnc_createFromConfig; +}] call CBA_fnc_addEventHandler; diff --git a/addons/medical_ai/XEH_preInit.sqf b/addons/medical_ai/XEH_preInit.sqf new file mode 100644 index 0000000000..a7feade1c3 --- /dev/null +++ b/addons/medical_ai/XEH_preInit.sqf @@ -0,0 +1,7 @@ +#include "script_component.hpp" + +ADDON = false; + +#include "XEH_PREP.hpp" + +ADDON = true; diff --git a/addons/medical_ai/XEH_preStart.sqf b/addons/medical_ai/XEH_preStart.sqf new file mode 100644 index 0000000000..022888575e --- /dev/null +++ b/addons/medical_ai/XEH_preStart.sqf @@ -0,0 +1,3 @@ +#include "script_component.hpp" + +#include "XEH_PREP.hpp" diff --git a/addons/medical_ai/config.cpp b/addons/medical_ai/config.cpp new file mode 100644 index 0000000000..e6f81293f6 --- /dev/null +++ b/addons/medical_ai/config.cpp @@ -0,0 +1,18 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + name = COMPONENT_NAME; + units[] = {}; + weapons[] = {}; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = {"ace_medical"}; + author = ECSTRING(common,ACETeam); + authors[] = {"BaerMitUmlaut"}; + url = ECSTRING(main,URL); + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" +#include "StateMachine.hpp" diff --git a/addons/medical_ai/functions/fnc_canRequestMedic.sqf b/addons/medical_ai/functions/fnc_canRequestMedic.sqf new file mode 100644 index 0000000000..a73b7a7822 --- /dev/null +++ b/addons/medical_ai/functions/fnc_canRequestMedic.sqf @@ -0,0 +1,27 @@ +/* + * Author: BaerMitUmlaut + * Checks if there is a medic available in the unit's group. + * + * Arguments: + * None + * + * Return Value: + * Can request medic + * + * Public: No + */ +#include "script_component.hpp" + +// Note: Although an unconscious unit cannot call for a medic itself, +// we ignore this here. We need to "notice" the medic that he should +// treat other units, or else he won't do anything on his own. + +if ([_this] call EFUNC(medical,isMedic) || {vehicle _this != _this}) exitWith {false}; + +{ + if ([_x] call EFUNC(medical,isMedic)) exitWith { + _this setVariable [QGVAR(assignedMedic), _x]; + true + }; + false +} forEach (units _this); diff --git a/addons/medical_ai/functions/fnc_healSelf.sqf b/addons/medical_ai/functions/fnc_healSelf.sqf new file mode 100644 index 0000000000..7a1091b315 --- /dev/null +++ b/addons/medical_ai/functions/fnc_healSelf.sqf @@ -0,0 +1,55 @@ +/* + * Author: BaerMitUmlaut + * Makes the unit heal itself. + * + * Arguments: + * None + * + * Return Value: + * Nothing + * + * Public: No + */ +#include "script_component.hpp" + +// Player will have to do this manually of course +if (isPlayer _this) exitWith {}; +// Can't heal self when unconscious +if (_this getVariable ["ACE_isUnconscious", false]) exitWith {}; +// Check if we're still treating +if ((_this getVariable [QGVAR(treatmentOverAt), CBA_missionTime]) > CBA_missionTime) exitWith {}; + +private _needsBandaging = ([_this] call EFUNC(medical,getBloodLoss)) > 0; +private _needsMorphine = (_this getVariable [QEGVAR(medical,pain), 0]) > 0.2; + +switch (true) do { + case _needsBandaging: { + // Select first wound and bandage it + private _openWounds = _this getVariable [QEGVAR(medical,openWounds), []]; + private _partIndex = { + _x params ["", "", "_index", "_amount", "_percentage"]; + if (_amount * _percentage > 0) exitWith { + _index + }; + } forEach _openWounds; + private _selection = ["head","body","hand_l","hand_r","leg_l","leg_r"] select _partIndex; + [_this, "Bandage", _selection] call EFUNC(medical,treatmentAdvanced_bandageLocal); + + #ifdef DEBUG_MODE_FULL + systemChat format ["%1 is bandaging selection %2", _this, _selection]; + #endif + + // Play animation + [_this, true, true] call FUNC(playTreatmentAnim); + _this setVariable [QGVAR(treatmentOverAt), CBA_missionTime + 5]; + }; + case _needsMorphine: { + [_this] call EFUNC(medical,treatmentBasic_morphineLocal); + [_this, false, true] call FUNC(playTreatmentAnim); + _this setVariable [QGVAR(treatmentOverAt), CBA_missionTime + 2]; + + #ifdef DEBUG_MODE_FULL + systemChat format ["%1 is giving himself morphine", _this]; + #endif + }; +}; diff --git a/addons/medical_ai/functions/fnc_healUnit.sqf b/addons/medical_ai/functions/fnc_healUnit.sqf new file mode 100644 index 0000000000..52b0c51e6e --- /dev/null +++ b/addons/medical_ai/functions/fnc_healUnit.sqf @@ -0,0 +1,84 @@ +/* + * Author: BaerMitUmlaut + * Makes a medic heal the next unit that needs treatment. + * + * Arguments: + * None + * + * Return Value: + * Nothing + * + * Public: No + */ +#include "script_component.hpp" + +// Can't heal other units when unconscious +if (_this getVariable ["ACE_isUnconscious", false]) exitWith {}; +// Check if we're still treating +if ((_this getVariable [QGVAR(treatmentOverAt), CBA_missionTime]) > CBA_missionTime) exitWith {}; + +// Find next unit to treat +private _healQueue = _this getVariable [QGVAR(healQueue), []]; +private _target = _healQueue select 0; + +// If unit died or was healed, be lazy and wait for the next tick +if (isNull _target || {!alive _target} || {!(_target call FUNC(isInjured))}) exitWith { + _target forceSpeed -1; + _healQueue deleteAt 0; + _this getVariable [QGVAR(healQueue), _healQueue]; + _this forceSpeed -1; + // return to formation instead of going where the injured unit was if it healed itself in the mean time + _this doFollow leader _this; + + #ifdef DEBUG_MODE_FULL + systemChat format ["%1 finished healing %2", _this, _target]; + #endif +}; + +// Move to target... +if (_this distance _target > 2) exitWith { + if !(_this getVariable [QGVAR(movingToInjured), false]) then { + _this setVariable [QGVAR(movingToInjured), true]; + _this doMove getPosATL _target; + }; +}; +_this setVariable [QGVAR(movingToInjured), false]; + +// ...and make sure medic and target don't move +_this forceSpeed 0; +_target forceSpeed 0; + +private _needsBandaging = ([_target] call EFUNC(medical,getBloodLoss)) > 0; +private _needsMorphine = (_target getVariable [QEGVAR(medical,pain), 0]) > 0.2; + +switch (true) do { + case _needsBandaging: { + // Select first wound and bandage it + private _openWounds = _target getVariable [QEGVAR(medical,openWounds), []]; + private _partIndex = { + _x params ["", "", "_index", "_amount", "_percentage"]; + if (_amount * _percentage > 0) exitWith { + _index + }; + } forEach _openWounds; + private _selection = ["head","body","hand_l","hand_r","leg_l","leg_r"] select _partIndex; + [_target, "Bandage", _selection] call EFUNC(medical,treatmentAdvanced_bandageLocal); + + #ifdef DEBUG_MODE_FULL + systemChat format ["%1 is bandaging selection %2 on %3", _this, _selection, _target]; + #endif + + // Play animation + [_this, true, false] call FUNC(playTreatmentAnim); + _this setVariable [QGVAR(treatmentOverAt), CBA_missionTime + 5]; + }; + case _needsMorphine: { + [_target] call EFUNC(medical,treatmentBasic_morphineLocal); + [_this, false, false] call FUNC(playTreatmentAnim); + _this setVariable [QGVAR(treatmentOverAt), CBA_missionTime + 2]; + + #ifdef DEBUG_MODE_FULL + systemChat format ["%1 is giving %2 morphine", _this, _target]; + #endif + }; +}; diff --git a/addons/medical_ai/functions/fnc_isInjured.sqf b/addons/medical_ai/functions/fnc_isInjured.sqf new file mode 100644 index 0000000000..6852fc1c58 --- /dev/null +++ b/addons/medical_ai/functions/fnc_isInjured.sqf @@ -0,0 +1,22 @@ +/* + * Author: BaerMitUmlaut + * Checks if a unit needs treatment. + * + * Arguments: + * None + * + * Return Value: + * Does unit need treatment + * + * Public: No + */ +#include "script_component.hpp" + +if !(alive _this) exitWith {false}; + +private _bloodLoss = [_this] call EFUNC(medical,getBloodLoss); +private _pain = _this getVariable [QEGVAR(medical,pain), 0]; +// Advanced only? +// private _heartRate = _this getVariable [QEGVAR(medical,heartRate), 70]; + +(_bloodLoss > 0) || {_pain > 0.2} // || {_heartRate > 100} || {_heartRate < 40} diff --git a/addons/medical_ai/functions/fnc_isSafe.sqf b/addons/medical_ai/functions/fnc_isSafe.sqf new file mode 100644 index 0000000000..92bbc60587 --- /dev/null +++ b/addons/medical_ai/functions/fnc_isSafe.sqf @@ -0,0 +1,15 @@ +/* + * Author: BaerMitUmlaut + * Checks if a unit is currently considered safe enough to treat itself. + * + * Arguments: + * None + * + * Return Value: + * Is unit safe enough + * + * Public: No + */ +#include "script_component.hpp" + +(getSuppression _this == 0) && {CBA_missionTime - (_this getVariable [QGVAR(lastFired), -30]) > 30} diff --git a/addons/medical_ai/functions/fnc_playTreatmentAnim.sqf b/addons/medical_ai/functions/fnc_playTreatmentAnim.sqf new file mode 100644 index 0000000000..93a784922d --- /dev/null +++ b/addons/medical_ai/functions/fnc_playTreatmentAnim.sqf @@ -0,0 +1,38 @@ +/* + * Author: BaerMitUmlaut + * Plays the corresponding treatment animation. + * + * Arguments: + * 0: Unit + * 1: Is bandage + * 2: Is self treatment + * + * Return Value: + * Nothing + * + * Public: No + */ +#include "script_component.hpp" +params ["_unit", "_isBandage", "_isSelfTreatment"]; + +if (vehicle _unit != _unit) exitWith {}; + +private _animConfig = if (_isBandage) then { + configFile >> "ACE_Medical_Actions" >> "Basic" >> "Bandage"; +} else { + configFile >> "ACE_Medical_Actions" >> "Basic" >> "Morphine"; +}; + +private _configProperty = "animationCaller"; +if (_isSelfTreatment) then { + _configProperty = _configProperty + "Self"; +}; +if (stance _unit == "PRONE") then { + _configProperty = _configProperty + "Prone"; +}; + +private _anim = getText (_animConfig >> _configProperty); +private _wpn = ["non", "rfl", "pst"] select (1 + ([primaryWeapon _unit, handgunWeapon _unit] find (currentWeapon _unit))); +_anim = [_anim, "[wpn]", _wpn] call CBA_fnc_replace; + +[_unit, _anim] call EFUNC(common,doAnimation); diff --git a/addons/medical_ai/functions/fnc_requestMedic.sqf b/addons/medical_ai/functions/fnc_requestMedic.sqf new file mode 100644 index 0000000000..f669b06975 --- /dev/null +++ b/addons/medical_ai/functions/fnc_requestMedic.sqf @@ -0,0 +1,22 @@ +/* + * Author: BaerMitUmlaut + * Sends a request to the units assigned medic to heal it. + * + * Arguments: + * None + * + * Return Value: + * Nothing + * + * Public: No + */ +#include "script_component.hpp" + +private _assignedMedic = _this getVariable QGVAR(assignedMedic); +private _healQueue = _assignedMedic getVariable [QGVAR(healQueue), []]; +_healQueue pushBack _this; +_assignedMedic setVariable [QGVAR(healQueue), _healQueue]; + +#ifdef DEBUG_MODE_FULL + systemChat format ["%1 requested %2 for medical treatment", _this, _assignedMedic]; +#endif diff --git a/addons/medical_ai/functions/fnc_wasRequested.sqf b/addons/medical_ai/functions/fnc_wasRequested.sqf new file mode 100644 index 0000000000..2b86423be8 --- /dev/null +++ b/addons/medical_ai/functions/fnc_wasRequested.sqf @@ -0,0 +1,16 @@ +/* + * Author: BaerMitUmlaut + * Checks if the unit was requested to treat another unit. + * + * Arguments: + * None + * + * Return Value: + * Was requested + * + * Public: No + */ +#include "script_component.hpp" + +private _healQueue = _this getVariable [QGVAR(healQueue), []]; +!(_healQueue isEqualTo []) diff --git a/addons/medical_ai/functions/script_component.hpp b/addons/medical_ai/functions/script_component.hpp new file mode 100644 index 0000000000..e0adc75020 --- /dev/null +++ b/addons/medical_ai/functions/script_component.hpp @@ -0,0 +1 @@ +#include "\z\ace\addons\medical_ai\script_component.hpp" diff --git a/addons/medical_ai/script_component.hpp b/addons/medical_ai/script_component.hpp new file mode 100644 index 0000000000..f50197ee67 --- /dev/null +++ b/addons/medical_ai/script_component.hpp @@ -0,0 +1,18 @@ +#define COMPONENT medical_ai +#define COMPONENT_BEAUTIFIED Medical AI +#include "\z\ace\addons\main\script_mod.hpp" + +// #define DEBUG_ENABLED_MEDICAL_AI +// #define DISABLE_COMPILE_CACHE +// #define CBA_DEBUG_SYNCHRONOUS +// #define ENABLE_PERFORMANCE_COUNTERS + +#ifdef DEBUG_ENABLED_MEDICAL_AI + #define DEBUG_MODE_FULL +#endif + +#ifdef DEBUG_SETTINGS_MEDICAL_AI + #define DEBUG_SETTINGS DEBUG_SETTINGS_MEDICAL_AI +#endif + +#include "\z\ace\addons\main\script_macros.hpp"