Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Medical AI #4311

Merged
merged 13 commits into from
Sep 4, 2016
1 change: 1 addition & 0 deletions addons/medical_ai/$PBOPREFIX$
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
z\ace\addons\medical_ai
17 changes: 17 additions & 0 deletions addons/medical_ai/CfgEventHandlers.hpp
Original file line number Diff line number Diff line change
@@ -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));
};
};
10 changes: 10 additions & 0 deletions addons/medical_ai/README.md
Original file line number Diff line number Diff line change
@@ -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)
86 changes: 86 additions & 0 deletions addons/medical_ai/StateMachine.hpp
Original file line number Diff line number Diff line change
@@ -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} \
);
};
};
};
8 changes: 8 additions & 0 deletions addons/medical_ai/XEH_PREP.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PREP(canRequestMedic);
PREP(healSelf);
PREP(healUnit);
PREP(isInjured);
PREP(isSafe);
PREP(playTreatmentAnim);
PREP(requestMedic);
PREP(wasRequested);
31 changes: 31 additions & 0 deletions addons/medical_ai/XEH_postInit.sqf
Original file line number Diff line number Diff line change
@@ -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", {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a global event. Some kind of locality check or duplication check will be necessary in this event handler. No need to add to the healQueue if the unit is already in that queue

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), []];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the unit is already being healed at this moment ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't matter, the medic checks before moving to the unit if it still needs healing.

_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;
7 changes: 7 additions & 0 deletions addons/medical_ai/XEH_preInit.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "script_component.hpp"

ADDON = false;

#include "XEH_PREP.hpp"

ADDON = true;
3 changes: 3 additions & 0 deletions addons/medical_ai/XEH_preStart.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include "script_component.hpp"

#include "XEH_PREP.hpp"
18 changes: 18 additions & 0 deletions addons/medical_ai/config.cpp
Original file line number Diff line number Diff line change
@@ -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"
27 changes: 27 additions & 0 deletions addons/medical_ai/functions/fnc_canRequestMedic.sqf
Original file line number Diff line number Diff line change
@@ -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 <BOOL>
*
* 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);
55 changes: 55 additions & 0 deletions addons/medical_ai/functions/fnc_healSelf.sqf
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two conditions are the same in multiple places. Should we convert those to a macro?


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
};
};
84 changes: 84 additions & 0 deletions addons/medical_ai/functions/fnc_healUnit.sqf
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

@thojkooi thojkooi Sep 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a bit to much for an initial version, but shouldn't they find cover first, if under fire?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The medic will only be requested once the injured unit is considered safe, and the medic will only move to the injured unit when the medic is safe. The safety status of the injured unit might change in the mean time, but the medic simply aborts the process when he is not safe anymore.


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
};
};
22 changes: 22 additions & 0 deletions addons/medical_ai/functions/fnc_isInjured.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Author: BaerMitUmlaut
* Checks if a unit needs treatment.
*
* Arguments:
* None
*
* Return Value:
* Does unit need treatment <BOOL>
*
* 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}
15 changes: 15 additions & 0 deletions addons/medical_ai/functions/fnc_isSafe.sqf
Original file line number Diff line number Diff line change
@@ -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 <BOOL>
*
* Public: No
*/
#include "script_component.hpp"

(getSuppression _this == 0) && {CBA_missionTime - (_this getVariable [QGVAR(lastFired), -30]) > 30}
Loading