Medical - Rework wound handling (#8278)

- Add stackable wound handler system for easy 3rd party extensibility and overriding of default wound handler.
- Change mapping from wound type -> damage types, to damage type -> wound types. Improves the semantics and makes configuration easier to reason about.
- Allow damage types to influence wound properties (bleed, size, etc.) with configurable variance parameters.
- Allow configuration of wound type variance per damage type. Enabling more logically driven variance for sensible but still varied end results.
- Improve handling of non-selection-specific damage events. The wound handler now receives all incoming damages and may apply damage to multiple selections (previously only ever one) if the damage type is not configured to be selection specific (with new config property `selectionSpecific`).
- Add debug script for testing explosion damage events at varied ranges.
- Add custom fire wound handler.
This commit is contained in:
pterolatypus 2022-02-17 20:03:12 +00:00 committed by GitHub
parent 00553537dc
commit 73a7dbdc1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1053 additions and 388 deletions

View File

@ -9,7 +9,7 @@
* 2: Body part ("Head", "Body", "LeftArm", "RightArm", "LeftLeg", "RightLeg") <STRING>
* 3: Projectile Type <STRING>
* 4: Source <OBJECT>
* 5: Non-directional damage source array <ARRAY> (default: [])
* 5: Unused parameter maintained for backwards compatibility <ARRAY> (default: [])
* 6: Override Invulnerability <BOOL> (default: true)
*
* Return Value:
@ -29,7 +29,7 @@ params [
["_bodyPart", "", [""]],
["_typeOfDamage", "", [""]],
["_instigator", objNull, [objNull]],
["_damageSelectionArray", [], [[]]],
"",
["_overrideInvuln", true, [true]]
];
TRACE_7("addDamageToUnit",_unit,_damageToAdd,_bodyPart,_typeOfDamage,_instigator,_damageSelectionArray,_overrideInvuln);
@ -48,10 +48,6 @@ if (!_overrideInvuln && {!((isDamageAllowed _unit) && {_unit getVariable [QEGVAR
// Extension is case sensitive and expects this format (different from ALL_BODY_PARTS)
_bodyPart = ["Head", "Body", "LeftArm", "RightArm", "LeftLeg", "RightLeg"] select _bodyPartIndex;
if (_damageSelectionArray isEqualTo []) then { // this will only be used if damage type is not location specific
_damageSelectionArray = [HITPOINT_INDEX_HEAD, 1, HITPOINT_INDEX_BODY, 1, HITPOINT_INDEX_LARM, 1, HITPOINT_INDEX_RARM, 1, HITPOINT_INDEX_LLEG, 1, HITPOINT_INDEX_RLEG, 1];
};
if (!isNull _instigator) then {
_unit setVariable [QEGVAR(medical,lastDamageSource), _instigator];
_unit setVariable [QEGVAR(medical,lastInstigator), _instigator];
@ -62,7 +58,7 @@ private _startDmg = +(_unit getVariable [QEGVAR(medical,bodyPartDamage), [0,0,0,
private _startPain = GET_PAIN(_unit);
#endif
[QEGVAR(medical,woundReceived), [_unit, _bodyPart, _damageToAdd, _instigator, _typeOfDamage, _damageSelectionArray]] call CBA_fnc_localEvent;
[QEGVAR(medical,woundReceived), [_unit, [[_damageToAdd, _bodyPart, _damageToAdd]], _instigator, _typeOfDamage]] call CBA_fnc_localEvent;
#ifdef DEBUG_TESTRESULTS
private _endDmg = _unit getVariable [QEGVAR(medical,bodyPartDamage), [0,0,0,0,0,0]];

View File

@ -18,7 +18,8 @@
* Public: No
*/
params ["_unit", "", "_damage", "_shooter"];
params ["_unit", "_allDamages", "_shooter"];
(_allDamages select 0) params ["_damage", ""];
// Don't bleed when players only and a non-player unit is wounded
if (GVAR(enabledFor) == BLOOD_ONLY_PLAYERS && {!isPlayer _unit && {_unit != ACE_player}}) exitWith {};

View File

@ -7,136 +7,299 @@ class ACE_Medical_Injuries {
// Source: Scarle
// Also called scrapes, they occur when the skin is rubbed away by friction against another rough surface (e.g. rope burns and skinned knees).
class Abrasion {
causes[] = {"falling", "ropeburn", "vehiclecrash", "collision", "unknown"};
bleeding = 0.001;
pain = 0.4;
minDamage = 0.01;
maxDamage = 0.30;
};
// Occur when an entire structure or part of it is forcibly pulled away, such as the loss of a permanent tooth or an ear lobe. Explosions, gunshots, and animal bites may cause avulsions.
class Avulsion {
causes[] = {"explosive", "vehiclecrash", "collision", "grenade", "shell", "bullet", "backblast", "bite"};
bleeding = 0.1;
pain = 1.0;
minDamage = 0.01;
causeLimping = 1;
};
// Also called bruises, these are the result of a forceful trauma that injures an internal structure without breaking the skin. Blows to the chest, abdomen, or head with a blunt instrument (e.g. a football or a fist) can cause contusions.
class Contusion {
causes[] = {"bullet", "backblast", "punch", "vehiclecrash", "collision", "falling"};
bleeding = 0;
pain = 0.3;
minDamage = 0.02;
maxDamage = 0.35;
};
// Occur when a heavy object falls onto a person, splitting the skin and shattering or tearing underlying structures.
class Crush {
causes[] = {"falling", "vehiclecrash", "collision", "punch", "unknown"};
bleeding = 0.05;
pain = 0.8;
minDamage = 0.1;
causeLimping = 1;
causeFracture = 1;
};
// Slicing wounds made with a sharp instrument, leaving even edges. They may be as minimal as a paper cut or as significant as a surgical incision.
class Cut {
causes[] = {"vehiclecrash", "collision", "grenade", "explosive", "shell", "backblast", "stab", "unknown"};
bleeding = 0.01;
pain = 0.1;
minDamage = 0.1;
};
// Also called tears, these are separating wounds that produce ragged edges. They are produced by a tremendous force against the body, either from an internal source as in childbirth, or from an external source like a punch.
class Laceration {
causes[] = {"vehiclecrash", "collision", "punch"};
bleeding = 0.05;
pain = 0.2;
minDamage = 0.01;
};
// Also called velocity wounds, they are caused by an object entering the body at a high speed, typically a bullet or small peices of shrapnel.
class VelocityWound {
causes[] = {"bullet", "grenade","explosive", "shell", "unknown"};
bleeding = 0.2;
pain = 0.9;
minDamage = 0.35;
causeLimping = 1;
causeFracture = 1;
};
// Deep, narrow wounds produced by sharp objects such as nails, knives, and broken glass.
class PunctureWound {
causes[] = {"stab", "grenade"};
bleeding = 0.05;
pain = 0.4;
minDamage = 0.02;
causeLimping = 1;
};
// Pain wound that is caused by making or being in contact with heat
class ThermalBurn {
causes[] = {"burn"};
bleeding = 0;
pain = 0.7;
minDamage = 0;
};
};
class damageTypes {
// thresholds[] {{<min damage>, <max number of wounds>}, {...}}
// thresholds[] {{<damage>, <number of wounds>}, {...}}
// if damage is between two points, number is interpolated and then rounded by chance based on the decimal part
// e.g. a value of 2.7 has 70% chance to give 3 and 30% to give 2
// put damage values in descending order; uses the first value found that is below the wound damage, and the point immediately before that
thresholds[] = {{0.1, 1}};
// if 1, wounds are only applied to the hitpoint that took the most damage. othewrise, wounds are applied to all damaged hitpoints
selectionSpecific = 1;
// list of damage handlers, which will be called in reverse order
// each entry should be a SQF expression that returns a function
// this can also be overridden for each damage type
class woundHandlers {
ADDON = QFUNC(woundsHandlerActive);
};
class bullet {
// above damage, amount. Put the highest threshold to the left and lower the threshold with the elements to the right of it.
thresholds[] = {{0.1, 1}};
// bullets only create multiple wounds when the damage is very high
thresholds[] = {{20, 10}, {4.5, 2}, {3, 1}, {0, 1}};
selectionSpecific = 1;
class Avulsion {
// at damage, weight. between points, weight is interpolated then wound is chosen by weighted random.
// as with thresholds, but result is not rounded (decimal values used as-is)
weighting[] = {{1, 1}, {0.35, 0}};
/*
damageMultiplier = 1;
sizeMultiplier = 1;
bleedingMultiplier = 1;
painMultiplier = 1;
fractureMultiplier = 1;
*/
};
class Contusion {
weighting[] = {{0.35, 0}, {0.35, 1}};
// bruises caused by bullets hitting the plate are big
sizeMultiplier = 3.2;
// tone down the pain a tiny bit to compensate
painMultiplier = 0.8;
};
class VelocityWound {
// velocity wounds are only in the 0.35-1.5 range
weighting[] = {{1.5, 0}, {1.5, 1}, {0.35, 1}, {0.35, 0}};
// velocity wounds will tend to be medium or large
sizeMultiplier = 0.9;
};
};
class grenade {
thresholds[] = {{0.1, 3}, {0, 1}};
// at low damage numbers, chance to create no wounds - makes it a bit more random instead of consistently covering people in bruises
thresholds[] = {{20, 10}, {10, 5}, {4, 3}, {1.5, 2}, {0.8, 2}, {0.3, 1}, {0, 0}};
selectionSpecific = 0;
class Avulsion {
weighting[] = {{1.5, 1}, {1.1, 0}};
};
class VelocityWound {
weighting[] = {{1.5, 0}, {1.1, 1}, {0.7, 0}};
};
class PunctureWound {
weighting[] = {{0.9, 0}, {0.7, 1}, {0.35, 0}};
};
class Cut {
weighting[] = {{0.7, 0}, {0.35, 1}, {0.35, 0}};
};
class Contusion {
weighting[] = {{0.5, 0}, {0.35, 1}};
sizeMultiplier = 2;
painMultiplier = 0.9;
};
};
class explosive {
thresholds[] = {{1, 6}, {0.1, 4}, {0, 1}};
// explosives create more and smaller wounds than grenades
thresholds[] = {{20, 15}, {8, 7}, {2, 3}, {1.2, 2}, {0.4, 1}, {0,0}};
selectionSpecific = 0;
class Avulsion {
weighting[] = {{1, 1}, {0.8, 0}};
};
class Cut {
weighting[] = {{1.5, 0}, {0.35, 1}, {0, 0}};
};
class Contusion {
weighting[] = {{0.5, 0}, {0.35, 1}};
sizeMultiplier = 2;
painMultiplier = 0.9;
};
};
class shell {
thresholds[] = {{1, 7}, {0.1, 5}, {0, 1}};
// shells tend to involve big pieces of shrapnel, so create fewer and larger wounds
thresholds[] = {{20, 10}, {10, 5}, {4.5, 2}, {2, 2}, {0.8, 1}, {0.2, 1}, {0, 0}};
selectionSpecific = 0;
class Avulsion {
weighting[] = {{1.5, 1}, {1.1, 0}};
};
class VelocityWound {
weighting[] = {{1.5, 0}, {1.1, 1}, {0.7, 0}};
};
class PunctureWound {
weighting[] = {{0.9, 0}, {0.7, 1}, {0.35, 0}};
};
class Cut {
weighting[] = {{0.7, 0}, {0.35, 1}, {0.35, 0}};
};
class Contusion {
weighting[] = {{0.5, 0}, {0.35, 1}};
sizeMultiplier = 2;
painMultiplier = 0.9;
};
};
class vehiclecrash {
thresholds[] = {{1.5, 3}, {1, 2}, {0.05, 1}}; // prevent subdividing wounds past FRACTURE_DAMAGE_THRESHOLD to ensure limp/fractue is triggered
thresholds[] = {{1.5, 3}, {1.5, 2}, {1, 2}, {1, 1}, {0.05, 1}}; // prevent subdividing wounds past FRACTURE_DAMAGE_THRESHOLD to ensure limp/fractue is triggered
selectionSpecific = 0;
class woundHandlers: woundHandlers {
GVAR(vehiclecrash) = QFUNC(woundsHandlerVehiclecrash);
};
class Abrasion {
weighting[] = {{0.30, 0}, {0.30, 1}};
};
class Avulsion {
weighting[] = {{0.01, 1}, {0.01, 0}};
};
class Contusion {
weighting[] = {{0.35, 0}, {0.35, 1}};
};
class Crush {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
class Cut {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
class Laceration {
};
};
class collision {
thresholds[] = {{1.5, 3}, {1, 2}, {0.05, 1}}; // prevent subdividing wounds past FRACTURE_DAMAGE_THRESHOLD to ensure limp/fractue is triggered
thresholds[] = {{1.5, 3}, {1.5, 2}, {1, 2}, {1, 1}, {0.05, 1}}; // prevent subdividing wounds past FRACTURE_DAMAGE_THRESHOLD to ensure limp/fractue is triggered
selectionSpecific = 0;
class Abrasion {
weighting[] = {{0.30, 0}, {0.30, 1}};
};
class Avulsion {
weighting[] = {{1, 2}, {0.5, 0.5}, {0.5, 0}};
};
class Contusion {
weighting[] = {{0.35, 0}, {0.35, 1}};
};
class Crush {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
class Cut {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
class Laceration {
};
};
class backblast {
thresholds[] = {{1, 6}, {0.55, 5}, {0, 2}};
thresholds[] = {{1, 6}, {1, 5}, {0.55, 5}, {0.55, 2}, {0, 2}};
selectionSpecific = 0;
class Avulsion {
weighting[] = {{0.30, 0}, {0.30, 1}};
};
class Contusion {
weighting[] = {{0.35, 0}, {0.35, 1}};
};
class Cut {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
};
class stab {
thresholds[] = {{0.1, 1}};
thresholds[] = {{0.1, 1}, {0.1, 0}};
selectionSpecific = 1;
class Cut {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
class PunctureWound {
weighting[] = {{0.02, 1}, {0.02, 0}};
};
};
class punch {
thresholds[] = {{0.1, 1}};
thresholds[] = {{0.1, 1}, {0.1, 0}};
selectionSpecific = 1;
class Contusion {
weighting[] = {{0.35, 0}, {0.35, 1}};
};
class Crush {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
class Laceration {
};
};
class falling {
thresholds[] = {{1.5, 3}, {1, 2}, {0.05, 1}}; // prevent subdividing wounds past FRACTURE_DAMAGE_THRESHOLD to ensure limp/fractue is triggered
thresholds[] = {{1.5, 3}, {1.5, 2}, {1, 2}, {1, 1}, {0.05, 1}}; // prevent subdividing wounds past FRACTURE_DAMAGE_THRESHOLD to ensure limp/fractue is triggered
selectionSpecific = 0;
class Abrasion {
weighting[] = {{0.30, 0}, {0.30, 1}};
};
class Contusion {
weighting[] = {{0.35, 0}, {0.35, 1}};
};
class Crush {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
};
class ropeburn {
thresholds[] = {{0.1, 1}};
thresholds[] = {{0.1, 1}, {0.1, 0}};
selectionSpecific = 1;
class Abrasion {
weighting[] = {{0.30, 1}};
};
};
class drowning {
//No related wounds as drowning should not cause wounds/bleeding. Can be extended for internal injuries if they are added.
thresholds[] = {{0, 0}};
class woundHandlers {};
};
class fire {
// custom handling for environmental fire sources
// passes damage to "burn" so doesn't need its own wound stats
class woundHandlers {
ADDON = QFUNC(woundsHandlerBurning);
};
};
class burn {
thresholds[] = {{0, 1}};
selectionSpecific = 0;
class ThermalBurn {
weighting[] = {{0, 1}};
};
//No related wounds as drowning should not cause wounds/bleeding. Can be extended for internal injuries if they are added.
class drowning {
thresholds[] = {{0, 0}};
};
class unknown {
thresholds[] = {{0.1, 1}};
thresholds[] = {{0.1, 1}, {0.1, 0}};
class Abrasion {
weighting[] = {{0.30, 0}, {0.30, 1}};
};
class Cut {
weighting[] = {{0.1, 1}, {0.1, 0}};
};
class VelocityWound {
weighting[] = {{0.35, 1}, {0.35, 0}};
};
};
};
};

View File

@ -1,6 +1,12 @@
PREP(debug_explosiveTest);
PREP(determineIfFatal);
PREP(getTypeOfDamage);
PREP(handleIncapacitation);
PREP(interpolatePoints);
PREP(parseConfigForInjuries);
PREP(parseWoundHandlersCfg);
PREP(woundReceived);
PREP(woundsHandler);
PREP(woundsHandlerBurning);
PREP(woundsHandlerSQF);
PREP(woundsHandlerVehiclecrash);

View File

@ -8,14 +8,6 @@ PREP_RECOMPILE_END;
#include "initSettings.sqf"
call FUNC(parseConfigForInjuries);
addMissionEventHandler ["Loaded",{
INFO("Mission Loaded - Reloading medical configs for extension");
// Reload configs into extension (handle full game restart)
call FUNC(parseConfigForInjuries);
}];
// decide which woundsHandler to use by whether the extension is present or not
// if ("ace_medical" callExtension "version" != "") then {
@ -25,11 +17,14 @@ addMissionEventHandler ["Loaded",{
DFUNC(woundsHandlerActive) = LINKFUNC(woundsHandlerSQF);
// };
[QEGVAR(medical,woundReceived), {
params ["_unit", "_woundedHitPoint", "_receivedDamage", "", "_ammo", "_damageSelectionArray"];
call FUNC(parseConfigForInjuries);
private _typeOfDamage = _ammo call FUNC(getTypeOfDamage);
[_unit, _woundedHitPoint, _receivedDamage, _typeOfDamage, _damageSelectionArray] call FUNC(woundsHandlerActive);
}] call CBA_fnc_addEventHandler;
addMissionEventHandler ["Loaded",{
INFO("Mission Loaded - Reloading medical configs for extension");
// Reload configs into extension (handle full game restart)
call FUNC(parseConfigForInjuries);
}];
[QEGVAR(medical,woundReceived), LINKFUNC(woundReceived)] call CBA_fnc_addEventHandler;
ADDON = true;

View File

@ -0,0 +1,63 @@
#include "script_component.hpp"
/*
* Author: Pterolatypus
* Testing function that spawns AI units in a spiral around the given point and optionally spawns a projectile at the center
* Used for observing the effects of explosive munitions
*
* Arguments:
* 0: Center position, format PositionAGL <ARRAY>
* 1: Distance to spawn units <ARRAY>
* 0: Min (default: 1)
* 1: Max (default: 10)
* 2: Step (default: 1)
* 2: Unit class to spawn <STRING> (default: "B_Soldier_F")
* 3: Ammo class to spawn, "" or nil to skip <STRING> (default: "")
* 4: Delay between unit placement and ammo spawning in seconds <NUMBER> (default: 1)
* 5: Function to run on each unit that is spawned (optional) <CODE> params [_unit, _center, _ammoClass]
*
* ReturnValue:
* Nothing
*
* Example:
* [position player, [20, 80, 5]] call ace_medical_damage_fnc_debug_explosiveTest
*
* Public: No
*/
params [
"_center",
["_distances", []],
["_unitClass", "B_Soldier_F"],
["_ammoClass", ""],
["_delay", 1],
"_initCode"
];
_distances params [["_min", 1], ["_max", 10], ["_step", 1]];
if (isNil "_center") exitwith {};
_max = _max max _min;
private _nSteps = 0 max ceil ((_max - _min) / _step);
private _angleStep = 360 / (_nSteps + 1);
for "_distance" from _min to _max step _step do {
private _i = (_distance - _min) / _step;
private _angle = _i * _angleStep;
private _offset = [_distance * sin _angle, _distance * cos _angle, 0];
private _position = _center vectorAdd _offset;
private _unit = (createGroup west) createUnit [_unitClass, _position, [], 0, "CAN_COLLIDE"];
if !(isNil "_initCode") then {
[_unit, _center, _ammoClass] call _initCode;
};
};
// spawn the ammo above the ground falling. necessary for shells, doesn't cause problems for grenades etc.
if (_ammoClass != "") then {
[{
params ["_ammoClass", "_center"];
private _position = _center vectorAdd [0, 0, 5];
private _obj = _ammoClass createVehicle _position;
_object setVectorDirAndUp [[0, 0, -1], [0, 1, 0]];
_obj setVelocity [0, 0, -20];
}, [_ammoClass, _center], _delay] call CBA_fnc_waitAndExecute;
};

View File

@ -17,7 +17,7 @@
params ["_typeOfProjectile"];
private _damageType = GVAR(damageTypeCache) getVariable _typeOfProjectile;
private _damageType = GVAR(damageTypeCache) get _typeOfProjectile;
if (isNil "_damageType") then {
if (isText (configFile >> "CfgAmmo" >> _typeOfProjectile >> "ACE_damageType")) then {
@ -28,13 +28,13 @@ if (isNil "_damageType") then {
};
// config may define an invalid damage type
if (isNil {GVAR(allDamageTypesData) getVariable _damageType}) then {
if !(_damageType in GVAR(damageTypeDetails)) then {
WARNING_2("Damage type [%1] for ammo [%2] not found",_typeOfDamage,_typeOfProjectile);
_damageType = "unknown";
};
TRACE_2("getTypeOfDamage caching",_typeOfProjectile,_damageType);
GVAR(damageTypeCache) setVariable [_typeOfProjectile, _damageType];
GVAR(damageTypeCache) set [_typeOfProjectile, _damageType];
};
_damageType // return

View File

@ -0,0 +1,41 @@
#include "script_component.hpp"
/*
* Author: Pterolatypus
* Returns the image of a value on a linear piecewise function defined by given points
* Force integer causes decimals to be rounded up or down by chance based on their decimal part, i.e. 2.7 has a 70% chance to return 3 and 30% to return 2
*
* Arguments:
* 0: Input value <NUMBER>
* 1: Function points, must be in descending order by X (input) value <ARRAY>
* 2: Whether to force integer <BOOLEAN>
*
* ReturnValue:
* Interpolated result <NUMBER>
*
* Example:
* [0.2, [[1,0], [0.5,1], [0,0]]] call ace_medical_damage_fnc_interpolatePoints
*
* Public: No
*/
params ["_input", "_points", ["_randomRound", false]];
if (count _points < 1) exitWith {
//TODO: sensible default/error value
0
};
if (count _points == 1) exitWith {_points select 0 select 1};
private _output = 0;
private _lower = _points findIf {(_x select 0) < _input};
if (_lower == 0) exitWith {_points select 0 select 1};
if (_lower == -1) exitWith {_points select (count _points - 1) select 1};
private _upper = _points select (_lower-1);
_lower = _points select _lower;
_output = linearConversion [_lower select 0, _upper select 0, _input, _lower select 1, _upper select 1, true];
if (_randomRound) then {
// chance to round up is equal to the decimal part
_output = ceil (_output - random 1);
};
_output //return

View File

@ -20,7 +20,7 @@ private _injuriesConfigRoot = configFile >> "ACE_Medical_Injuries";
// --- parse wounds
GVAR(woundClassNames) = [];
GVAR(woundClassNamesComplex) = []; // index = 10 * classID + category; [will contain nils] e.g. ["aMinor", "aMed", "aLarge", nil, nil..."bMinor"]
GVAR(woundsData) = [];
GVAR(woundDetails) = createHashMap;
private _woundsConfig = _injuriesConfigRoot >> "wounds";
private _classID = 0;
@ -32,53 +32,61 @@ private _classID = 0;
private _selections = GET_ARRAY(_entry >> "selections",["All"]);
private _bleeding = GET_NUMBER(_entry >> "bleeding",0);
private _pain = GET_NUMBER(_entry >> "pain",0);
private _minDamage = GET_NUMBER(_entry >> "minDamage",0);
private _maxDamage = GET_NUMBER(_entry >> "maxDamage",-1);
private _causes = GET_ARRAY(_entry >> "causes",[]);
private _causeLimping = GET_NUMBER(_entry >> "causeLimping",0) == 1;
private _causeFracture = GET_NUMBER(_entry >> "causeFracture",0) == 1;
if (_causes isNotEqualTo []) then {
private _details = [_selections, _bleeding, _pain, _causeLimping, _causeFracture];
GVAR(woundDetails) set [_className, _details];
GVAR(woundDetails) set [_classID, _details];
GVAR(woundClassNames) pushBack _className;
GVAR(woundsData) pushBack [_classID, _selections, _bleeding, _pain, [_minDamage, _maxDamage], _causes, _className, _causeLimping, _causeFracture];
{
GVAR(woundClassNamesComplex) set [10 * _classID + _forEachIndex, format ["%1%2", _className, _x]];
} forEach ["Minor", "Medium", "Large"];
_classID = _classID + 1;
};
} forEach configProperties [_woundsConfig, "isClass _x"];
// --- parse damage types
GVAR(allDamageTypesData) = [] call CBA_fnc_createNamespace;
GVAR(damageTypeDetails) = createHashMap;
// cache for ammunition -> damageType
GVAR(damageTypeCache) = [] call CBA_fnc_createNamespace;
GVAR(damageTypeCache) = createHashMap;
// minimum lethal damage collection, mapped to damageTypes
private _damageTypesConfig = _injuriesConfigRoot >> "damageTypes";
private _thresholdsDefault = getArray (_damageTypesConfig >> "thresholds");
private _selectionSpecificDefault = getNumber (_damageTypesConfig >> "selectionSpecific");
private _defaultWoundHandlers = [];
if (isClass (_damageTypesConfig >> "woundHandlers")) then {
_defaultWoundHandlers = [_damageTypesConfig >> "woundHandlers"] call FUNC(parseWoundHandlersCfg);
reverse _defaultWoundHandlers;
};
TRACE_1("Found default wound handlers", count _defaultWoundHandlers);
// Collect all available damage types from the config
{
private _entry = _x;
private _className = configName _entry;
// Check if this type is in the causes of a wound class, if so, we will store the wound types for this damage type
private _woundTypes = [];
{
if (_className in (_x select 5)) then {
_woundTypes pushBack _x;
};
} forEach GVAR(woundsData);
if (_className == "woundHandlers") then {continue};
GVAR(damageTypeCache) set [_className, _className];
GVAR(damageTypeCache) set ["#"+_className, _className];
private _damageTypeSubClassConfig = _damageTypesConfig >> _className;
private _thresholds = GET_ARRAY(_damageTypeSubClassConfig >> "thresholds",_thresholdsDefault);
private _selectionSpecific = GET_NUMBER(_damageTypeSubClassConfig >> "selectionSpecific",_selectionSpecificDefault);
GVAR(allDamageTypesData) setVariable [_className, [_thresholds, _selectionSpecific > 0, _woundTypes]];
GVAR(damageTypeCache) setVariable [_className, _className];
GVAR(damageTypeCache) setVariable ["#"+_className, _className];
private _woundHandlers = [];
if (isClass (_damageTypeSubClassConfig >> "woundHandlers")) then {
_woundHandlers = [_damageTypeSubClassConfig >> "woundHandlers"] call FUNC(parseWoundHandlersCfg);
reverse _woundHandlers;
TRACE_2("Damage type found wound handlers", _className, count _woundHandlers);
} else {
_woundHandlers = _defaultWoundHandlers;
TRACE_1("Damage type has no wound handlers, using default", _className);
};
/*
// extension loading
@ -99,6 +107,26 @@ private _selectionSpecificDefault = getNumber (_damageTypesConfig >> "selectionS
// private _extensionRes = "ace_medical" callExtension _extensionArgs;
// TRACE_1("",_extensionRes);
*/
// parse config for each wound this damage type can cause
private _damageWoundDetails = [];
{
private _woundType = configName _x;
if (_woundType == "woundHandlers") then {continue};
if (_woundType in GVAR(woundDetails)) then {
private _weighting = GET_ARRAY(_x >> "weighting",ARR_2([[0,1]]));
private _dmgMulti = GET_NUMBER(_x >> "damageMultiplier", 1);
private _bleedMulti = GET_NUMBER(_x >> "bleedingMultiplier", 1);
private _sizeMulti = GET_NUMBER(_x >> "sizeMultiplier", 1);
private _painMulti = GET_NUMBER(_x >> "painMultiplier", 1);
private _fractureMulti = GET_NUMBER(_x >> "fractureMultiplier", 1);
_damageWoundDetails pushBack [_woundType, _weighting, _dmgMulti, _bleedMulti, _sizeMulti, _painMulti, _fractureMulti];
} else {
WARNING_2("Damage type %1 refers to wound %2, but it doesn't exist: skipping.",_className,configName _x);
};
} forEach configProperties [_damageTypeSubClassConfig, "isClass _x"];
GVAR(damageTypeDetails) set [_className, [_thresholds, _selectionSpecific, _woundHandlers, _damageWoundDetails]];
} forEach configProperties [_damageTypesConfig, "isClass _x"];
/*

View File

@ -0,0 +1,33 @@
#include "script_component.hpp"
/*
* Author: Pterolatypus
* Read a list of wound handler entries from config, accounting for inheritance
*
* Arguments:
* 0: The config class containing the entries <CONFIG>
*
* ReturnValue:
* None
*
* Example:
* [configFile >> "ace_medical_injuries" >> "damageTypes"] call ace_medical_damage_fnc_parseWoundHandlersCfg
*
* Public: No
*/
params ["_config"];
// read all valid entries from config and store
private _entries = [];
{
private _entryResult = call compile getText _x;
if !(isNil "_entryResult") then {
_entries pushBack _entryResult;
}
} forEach configProperties [_config, "isText _x", false];
private _parent = inheritsFrom _config;
if (isNull _parent) exitWith {_entries};
// recursive call for parent
// can't use configProperties for inheritance since it returns entries in the wrong order
([_parent] call FUNC(parseWoundHandlersCfg)) + _entries;

View File

@ -0,0 +1,35 @@
#include "script_component.hpp"
/*
* Author: Pterolatypus
* Handle woundReceived event and pass to individual wound handlers
*
* Arguments:
* 0: Unit That Was Hit <OBJECT>
* 1: Damage done to each body part <ARRAY>
* 2: Shooter <OBJECT>
* 3: Ammo classname or damage type <STRING>
*
* ReturnValue:
* None
*
* Example:
* [_target, [[0.5, "LeftLeg", 1]], _shooter, "B_65x39_Caseless"] call ace_medical_damage_fnc_woundReceived
*
* Public: No
*/
params ["_unit", "_allDamages", "_shooter", "_ammo"];
private _typeOfDamage = _ammo call FUNC(getTypeOfDamage);
if (_typeOfDamage in GVAR(damageTypeDetails)) then {
(GVAR(damageTypeDetails) get _typeOfDamage) params ["", "", "_woundHandlers"];
private _damageData = [_unit, _allDamages, _typeOfDamage];
{
_damageData = _damageData call _x;
TRACE_1("Wound handler returned", _damageData);
if !(_damageData isEqualType [] && {(count _damageData) >= 3}) exitWith {
TRACE_1("Return invalid, terminating wound handling", _damageData);
};
} forEach _woundHandlers;
};

View File

@ -27,7 +27,7 @@ if (_typeOfDamage isEqualTo "") then {
_typeOfDamage = "unknown";
};
if (isNil {GVAR(allDamageTypesData) getVariable _typeOfDamage} ) then {
if !(_typeOfDamage in GVAR(damageTypeDetails)) then {
_typeOfDamage = "unknown";
};

View File

@ -0,0 +1,50 @@
#include "script_component.hpp"
/*
* Author: Pterolatypus
* Custom wound handler for burns. Stores up small damage events until there's enough to create a wound.
*
* Arguments:
* 0: Unit That Was Hit <OBJECT>
* 1: Damage done to each body part <ARRAY>
* 2: Type of the damage done <STRING>
*
* Return Value:
* None
*
* Example:
* [player, [[0.5, "Body", 0.5]], "burning"] call ace_medical_damage_fnc_woundsHandlerBurning
*
* Public: No
*/
params ["_unit", "_allDamages", "_typeOfDamage"];
#define FIRE_DAMAGE_INTERVAL 1
{
_x params ["_damage", "_bodyPart"];
if (_bodyPart != "#structural") then {
continue
};
private _storedDamage = _unit getVariable [QGVAR(storedBurnDamage), 0];
private _newDamage = _storedDamage + _damage;
// schedule a task to convert stored damage to wounds after a delay
// the task resets stored damage to zero, so if it isn't currently zero that means there is a task already waiting
if (_storedDamage == 0 && _newDamage > 0) then {
[{
params ["_unit"];
_bodyPart = selectRandom ["body", "leftleg", "rightleg"];
private _storedDamage = _unit getVariable [QGVAR(storedBurnDamage), 0];
[QEGVAR(medical,woundReceived), [_unit, [[_storedDamage, _bodyPart, _storedDamage]], _unit, "burn"]] call CBA_fnc_localEvent;
_unit setVariable [QGVAR(storedBurnDamage), 0, true];
},
[_unit], FIRE_DAMAGE_INTERVAL] call CBA_fnc_waitAndExecute;
};
_unit setVariable [QGVAR(storedBurnDamage), _newDamage];
} forEach _allDamages;
[] //return, no further damage handling for this event

View File

@ -5,113 +5,110 @@
*
* Arguments:
* 0: Unit That Was Hit <OBJECT>
* 1: Name Of Body Part <STRING>
* 2: Amount Of Damage <NUMBER>
* 3: Type of the damage done <STRING>
* 4: Weighted array of damaged selections <ARRAY>
* 1: Damage done to each body part <ARRAY>
* 2: Type of the damage done <STRING>
*
* Return Value:
* None
*
* Example:
* [player, "Body", 0.5, "bullet", [1, 1]] call ace_medical_damage_fnc_woundsHandlerSQF
* [player, [[0.5, "Body", 1]], "bullet"] call ace_medical_damage_fnc_woundsHandlerSQF
*
* Public: No
*/
params ["_unit", "_bodyPart", "_damage", "_typeOfDamage", "_damageSelectionArray"];
TRACE_5("woundsHandlerSQF",_unit,_bodyPart,_damage,_typeOfDamage,_damageSelectionArray);
params ["_unit", "_allDamages", "_typeOfDamage"];
TRACE_4("woundsHandlerSQF",_unit,_allDamages,_typeOfDamage);
// Convert the selectionName to a number and ensure it is a valid selection.
private _bodyPartN = ALL_BODY_PARTS find toLower _bodyPart;
if (_bodyPartN < 0) exitWith { ERROR_1("invalid body part %1",_bodyPart); };
if ((_typeOfDamage isEqualTo "") || {isNil {GVAR(allDamageTypesData) getVariable _typeOfDamage}}) then {
WARNING_1("damage type [%1] not found",_typeOfDamage);
if !(_typeOfDamage in GVAR(damageTypeDetails)) then {
WARNING_1("damage type not found",_typeOfDamage);
_typeOfDamage = "unknown";
};
// Get the damage type information. Format: [typeDamage thresholds, selectionSpecific, woundTypes]
// WoundTypes are the available wounds for this damage type. Format [[classID, selections, bleedingRate, pain], ..]
private _damageTypeInfo = [GVAR(allDamageTypesData) getVariable _typeOfDamage] param [0, [[], false, []]];
_damageTypeInfo params ["_thresholds", "_isSelectionSpecific", "_woundTypes"];
// find the available injuries for this damage type and damage amount
private _highestPossibleSpot = -1;
private _highestPossibleDamage = -1;
private _allPossibleInjuries = [];
{
_x params ["", "_selections", "", "", "_damageExtrema"];
_damageExtrema params ["_minDamage", "_maxDamage"];
// Check if the damage is higher as the min damage for the specific injury
if (_damage >= _minDamage && {_damage <= _maxDamage || _maxDamage < 0}) then {
// Check if the injury can be applied to the given selection name
// if ("All" in _selections || {_bodyPart in _selections}) then { // @todo, this is case sensitive! [we have no injuries that use this, disabled for now]
// Find the wound which has the highest minimal damage, so we can use this later on for adding the correct injuries
if (_minDamage > _highestPossibleDamage) then {
_highestPossibleSpot = _forEachIndex;
_highestPossibleDamage = _minDamage;
};
// Store the valid possible injury for the damage type, damage amount and selection
_allPossibleInjuries pushBack _x;
// };
};
} forEach _woundTypes;
// No possible wounds available for this damage type or damage amount.
if (_highestPossibleSpot < 0) exitWith { TRACE_2("no wounds possible",_damage,_highestPossibleSpot); };
GVAR(damageTypeDetails) get _typeOfDamage params ["_thresholds", "_selectionSpecific", "", "_damageWoundDetails"];
// Administration for open wounds and ids
private _openWounds = GET_OPEN_WOUNDS(_unit);
private _createdWounds = false;
private _updateDamageEffects = false;
private _painLevel = 0;
private _critialDamage = false;
private _criticalDamage = false;
private _bodyPartDamage = _unit getVariable [QEGVAR(medical,bodyPartDamage), [0,0,0,0,0,0]];
private _bodyPartVisParams = [_unit, false, false, false, false]; // params array for EFUNC(medical_engine,updateBodyPartVisuals);
{
_x params ["_thresholdMinDam", "_thresholdWoundCount"];
if (_damage > _thresholdMinDam) exitWith {
private _woundDamage = _damage / (_thresholdWoundCount max 1); // If the damage creates multiple wounds
for "_i" from 1 to _thresholdWoundCount do {
// Find the injury we are going to add. Format [ classID, allowedSelections, bleedingRate, injuryPain]
private _oldInjury = if (random 1 < 0.15) then {
_woundTypes select _highestPossibleSpot
} else {
selectRandom _allPossibleInjuries
// process wounds separately for each body part hit
{ // forEach _allDamages
_x params ["_damage", "_bodyPart"];
// silently ignore structural damage
if (_bodyPart == "#structural") then {continue};
// Convert the selectionName to a number and ensure it is a valid selection.
private _bodyPartNToAdd = ALL_BODY_PARTS find toLower _bodyPart;
if (_bodyPartNToAdd < 0) then {
ERROR_1("invalid body part %1",_bodyPart);
continue
};
_oldInjury params ["_woundClassIDToAdd", "", "_injuryBleedingRate", "_injuryPain", "", "", "", "_causeLimping", "_causeFracture"];
// determine how many wounds to create
private _nWounds = [_damage, _thresholds, true] call FUNC(interpolatePoints);
if (_nWounds < 1) then {
TRACE_2("Damage created zero wounds",_damage,_typeOfDamage);
continue
};
private _dmgPerWound = _damage/_nWounds;
private _bodyPartNToAdd = if (_isSelectionSpecific) then {_bodyPartN} else {selectRandomWeighted _damageSelectionArray};
// find the available injuries for this damage type and damage amount
private _weightedWoundTypes = [];
{
private _weighting = _x select 1;
private _woundWeight = [_dmgPerWound, _weighting] call FUNC(interpolatePoints);
_weightedWoundTypes pushBack _x;
_weightedWoundTypes pushBack _woundWeight;
} forEach _damageWoundDetails;
if (_weightedWoundTypes isEqualTo []) then {
TRACE_2("No valid wounds",_damage,_typeOfDamage);
continue
};
for "_i" from 1 to _nWounds do {
// Select the injury we are going to add
selectRandomWeighted _weightedWoundTypes params ["_woundTypeToAdd", "", "_dmgMultiplier", "_bleedMultiplier", "_sizeMultiplier", "_painMultiplier", "_fractureMultiplier"];
if (isNil "_woundTypeToAdd") then {
WARNING_4("No valid wound types",_damage,_dmgPerWound,_typeOfDamage,_bodyPart);
continue
};
GVAR(woundDetails) get _woundTypeToAdd params ["","_injuryBleedingRate","_injuryPain","_causeLimping","_causeFracture"];
private _woundClassIDToAdd = GVAR(woundClassNames) find _woundTypeToAdd;
// Add a bit of random variance to wounds
private _woundDamage = _dmgPerWound * _dmgMultiplier * random [0.9, 1, 1.1];
_bodyPartDamage set [_bodyPartNToAdd, (_bodyPartDamage select _bodyPartNToAdd) + _woundDamage];
_bodyPartVisParams set [[1,2,3,3,4,4] select _bodyPartNToAdd, true]; // Mark the body part index needs updating
// Damage to limbs/head is scaled higher than torso by engine
// Anything above this value is guaranteed worst wound possible
private _worstDamage = [2, 1, 4, 4, 4, 4] select _bodyPartNToAdd;
private _worstDamage = 2;
// More wounds means more likely to get nasty wound
private _countModifier = 1 + random(_i - 1);
#define LARGE_WOUND_THRESHOLD 0.5
// Config specifies bleeding and pain for worst possible wound
// Worse wound correlates to higher damage, damage is not capped at 1
private _bleedModifier = linearConversion [0.1, _worstDamage, _woundDamage * _countModifier, 0.25, 1, true];
private _painModifier = (_bleedModifier * random [0.7, 1, 1.3]) min 1; // Pain isn't directly scaled to bleeding
private _woundSize = linearConversion [0.1, _worstDamage, _woundDamage * _sizeMultiplier, LARGE_WOUND_THRESHOLD^3, 1, true];
private _bleeding = _injuryBleedingRate * _bleedModifier;
private _pain = _injuryPain * _painModifier;
private _pain = _woundSize * _painMultiplier * _injuryPain;
_painLevel = _painLevel + _pain;
// wound category (minor [0.25-0.5], medium [0.5-0.75], large [0.75+])
private _category = floor linearConversion [0.25, 0.75, _bleedModifier, 0, 2, true];
private _bleeding = _woundSize * _bleedMultiplier * _injuryBleedingRate;
// large wounds are > LARGE_WOUND_THRESHOLD
// medium is > LARGE_WOUND_THRESHOLD^2
// minor is > LARGE_WOUND_THRESHOLD^3
private _category = 0 max (2 - floor (ln _woundSize / ln LARGE_WOUND_THRESHOLD)) min 2;
private _classComplex = 10 * _woundClassIDToAdd + _category;
@ -119,7 +116,7 @@ private _bodyPartVisParams = [_unit, false, false, false, false]; // params arra
private _injury = [_classComplex, _bodyPartNToAdd, 1, _bleeding, _woundDamage];
if (_bodyPartNToAdd == 0 || {_bodyPartNToAdd == 1 && {_woundDamage > PENETRATION_THRESHOLD}}) then {
_critialDamage = true;
_criticalDamage = true;
};
if ([_unit, _bodyPartNToAdd, _bodyPartDamage, _woundDamage] call FUNC(determineIfFatal)) then {
if (!isPlayer _unit || {random 1 < EGVAR(medical,deathChance)}) then {
@ -138,7 +135,7 @@ private _bodyPartVisParams = [_unit, false, false, false, false]; // params arra
&& {EGVAR(medical,fractures) > 0}
&& {_bodyPartNToAdd > 1}
&& {_woundDamage > FRACTURE_DAMAGE_THRESHOLD}
&& {random 1 < EGVAR(medical,fractureChance)}
&& {random 1 < (_fractureMultiplier * EGVAR(medical,fractureChance))}
): {
private _fractures = GET_FRACTURES(_unit);
_fractures set [_bodyPartNToAdd, 1];
@ -184,25 +181,34 @@ private _bodyPartVisParams = [_unit, false, false, false, false]; // params arra
TRACE_1("adding new wound",_injury);
_openWounds pushBack _injury;
};
_createdWounds = true;
};
// selection-specific damage only hits the first part
if (_selectionSpecific > 0) then {
break;
};
} forEach _thresholds;
} forEach _allDamages;
if (_updateDamageEffects) then {
[_unit] call EFUNC(medical_engine,updateDamageEffects);
};
_unit setVariable [VAR_OPEN_WOUNDS, _openWounds, true];
_unit setVariable [QEGVAR(medical,bodyPartDamage), _bodyPartDamage, true];
if (_createdWounds) then {
_unit setVariable [VAR_OPEN_WOUNDS, _openWounds, true];
_unit setVariable [QEGVAR(medical,bodyPartDamage), _bodyPartDamage, true];
[_unit] call EFUNC(medical_status,updateWoundBloodLoss);
[_unit] call EFUNC(medical_status,updateWoundBloodLoss);
_bodyPartVisParams call EFUNC(medical_engine,updateBodyPartVisuals);
_bodyPartVisParams call EFUNC(medical_engine,updateBodyPartVisuals);
[QEGVAR(medical,injured), [_unit, _painLevel]] call CBA_fnc_localEvent;
[QEGVAR(medical,injured), [_unit, _painLevel]] call CBA_fnc_localEvent;
if (_critialDamage || {_painLevel > PAIN_UNCONSCIOUS}) then {
if (_criticalDamage || {_painLevel > PAIN_UNCONSCIOUS}) then {
[_unit] call FUNC(handleIncapacitation);
};
TRACE_4("exit",_unit,_painLevel,GET_PAIN(_unit),GET_OPEN_WOUNDS(_unit));
};
TRACE_4("exit",_unit,_painLevel,GET_PAIN(_unit),GET_OPEN_WOUNDS(_unit));
[] //return, no further damage handling

View File

@ -0,0 +1,27 @@
#include "script_component.hpp"
/*
* Author: Pterolatypus
* Custom wound handler for vehicle crashes, sends damage to a random hitpoint
*
* Arguments:
* 0: Unit That Was Hit <OBJECT>
* 1: Damage done to each body part <ARRAY>
* 2: Type of the damage done <STRING>
*
* Return Value:
* None
*
* Example:
* [player, [[0.5, "#structural", 1.5]], "vehicleCrash"] call ace_medical_damage_fnc_woundsHandlerVehicleCrash
*
* Public: No
*/
params ["_unit", "_allDamages", "_typeOfDamage"];
// randomise all hit selections
private _newDamages = _allDamages apply {
[_x select 0, selectRandom ALL_BODY_PARTS, _x select 2];
};
TRACE_1("Vehicle crash handled, passing damage", _newDamages);
[_unit, _newDamages, _typeOfDamage] //return

View File

@ -4,7 +4,9 @@ class CfgVehicles {
class CAManBase: Man {
// General
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
@ -12,12 +14,16 @@ class CfgVehicles {
class B_Soldier_base_F;
class B_Soldier_04_f: B_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class B_Soldier_05_f: B_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
@ -25,12 +31,16 @@ class CfgVehicles {
class I_Soldier_base_F;
class I_Soldier_03_F: I_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class I_Soldier_04_F: I_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
@ -38,79 +48,107 @@ class CfgVehicles {
class SoldierEB;
class O_Soldier_base_F: SoldierEB {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class O_Soldier_02_F: O_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class O_officer_F: O_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class O_Soldier_diver_base_F: O_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
// Virtual Reality
class B_Soldier_VR_F: B_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class B_Protagonist_VR_F: B_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class O_Soldier_VR_F: O_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class I_Soldier_VR_F: I_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class I_Protagonist_VR_F: I_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class O_Protagonist_VR_F: O_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class C_man_1;
class C_Protagonist_VR_F: C_man_1 {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
// Civilians
class C_Soldier_VR_F: C_man_1 {
class HitPoints {
ADD_ACE_HITPOINTS(1,1);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
// APEX
class O_V_Soldier_Viper_F: O_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(3,3);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
class O_V_Soldier_base_F: O_Soldier_base_F {
class HitPoints {
ADD_ACE_HITPOINTS(3,3);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
@ -118,7 +156,9 @@ class CfgVehicles {
class I_E_Man_Base_F;
class I_E_Uniform_01_coveralls_F: I_E_Man_Base_F {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
};

View File

@ -31,8 +31,8 @@
#ifdef DEBUG_MODE_FULL
[QEGVAR(medical,woundReceived), {
params ["_unit", "_woundedHitPoint", "_receivedDamage", "_shooter", "_ammo"];
TRACE_5("wound",_unit,_woundedHitPoint, _receivedDamage, _shooter, _ammo);
params ["_unit", "_damages", "_shooter", "_ammo"];
TRACE_4("wound",_unit,_damages, _shooter, _ammo);
//systemChat str _this;
}] call CBA_fnc_addEventHandler;
#endif
@ -59,7 +59,7 @@
private _lethality = linearConversion [0, 25, (vectorMagnitude velocity _vehicle), 0.5, 1];
TRACE_2("air crash",_lethality,crew _vehicle);
{
[QEGVAR(medical,woundReceived), [_x, "Head", _lethality, _killer, "#vehiclecrash", [HITPOINT_INDEX_HEAD,1]], _x] call CBA_fnc_targetEvent;
[QEGVAR(medical,woundReceived), [_x, [[_lethality, "Head", _lethality]], _killer, "#vehiclecrash"], _x] call CBA_fnc_targetEvent;
} forEach (crew _vehicle);
}, true, ["ParachuteBase"]] call CBA_fnc_addClassEventHandler;

View File

@ -31,7 +31,7 @@ if (isNil QUOTE(FATAL_SUM_DAMAGE_WEIBULL_K) || isNil QUOTE(FATAL_SUM_DAMAGE_WEIB
};
// Cache for armor values of equipped items (vests etc)
GVAR(armorCache) = false call CBA_fnc_createNamespace;
GVAR(armorCache) = createHashMap;
// Hack for #3168 (units in static weapons do not take any damage):
// Doing a manual pre-load with a small distance seems to fix the LOD problems

View File

@ -19,22 +19,26 @@
params ["_item", "_hitpoint"];
private _key = format ["%1$%2", _item, _hitpoint];
private _armor = GVAR(armorCache) getVariable _key;
private _armor = GVAR(armorCache) get _key;
if (isNil "_armor") then {
TRACE_2("Cache miss",_item,_hitpoint);
if ("" in [_item, _hitpoint]) exitWith {
_armor = 0;
GVAR(armorCache) setVariable [_key, _armor];
GVAR(armorCache) set [_key, _armor];
};
private _itemInfo = configFile >> "CfgWeapons" >> _item >> "ItemInfo";
if (getNumber (_itemInfo >> "type") == TYPE_UNIFORM) then {
private _unitCfg = configFile >> "CfgVehicles" >> getText (_itemInfo >> "uniformClass");
if (_hitpoint == "#structural") then {
// TODO: I'm not sure if this should be multiplied by the base armor value or not
_armor = getNumber (_unitCfg >> "armorStructural");
} else {
private _entry = _unitCfg >> "HitPoints" >> _hitpoint;
_armor = getNumber (_unitCfg >> "armor") * getNumber (_entry >> "armor")
_armor = getNumber (_unitCfg >> "armor") * (1 max getNumber (_entry >> "armor"));
};
} else {
private _condition = format ["getText (_x >> 'hitpointName') == '%1'", _hitpoint];
private _entry = configProperties [_itemInfo >> "HitpointsProtectionInfo", _condition] param [0, configNull];
@ -42,7 +46,7 @@ if (isNil "_armor") then {
_armor = getNumber (_entry >> "armor");
};
GVAR(armorCache) setVariable [_key, _armor];
GVAR(armorCache) set [_key, _armor];
};
_armor // return

View File

@ -13,9 +13,6 @@
*
* Public: No
*/
// for travis
#define HIT_STRUCTURAL QGVAR($#structural)
params ["_unit", "_selection", "_damage", "_shooter", "_ammo", "_hitPointIndex", "_instigator", "_hitpoint"];
// HD sometimes triggers for remote units - ignore.
@ -38,13 +35,37 @@ private _newDamage = _damage - _oldDamage;
// Get armor value of hitpoint and calculate damage before armor
private _armor = [_unit, _hitpoint] call FUNC(getHitpointArmor);
private _realDamage = _newDamage * _armor;
// Damages are stored for "ace_hdbracket" event triggered last
_unit setVariable [format [QGVAR($%1), _hitPoint], [_realDamage, _newDamage]];
TRACE_3("Received hit",_hitpoint,_newDamage,_realDamage);
TRACE_4("Received hit",_hitpoint,_ammo,_newDamage,_realDamage);
// Engine damage to these hitpoints controls blood visuals, limping, weapon sway
// Handled in fnc_damageBodyPart, persist here
if (_hitPoint in ["hithead", "hitbody", "hithands", "hitlegs"]) exitWith {_oldDamage};
// Drowning doesn't fire the EH for each hitpoint so the "ace_hdbracket" code never runs
// Damage occurs in consistent increments
if (
_hitPoint isEqualTo "#structural" &&
{getOxygenRemaining _unit <= 0.5} &&
{_damage isEqualTo (_oldDamage + 0.005)}
) exitWith {
TRACE_5("Drowning",_unit,_shooter,_instigator,_damage,_newDamage);
[QEGVAR(medical,woundReceived), [_unit, [[_newDamage, "Body", _newDamage]], _unit, "drowning"]] call CBA_fnc_localEvent;
0
};
// Crashing a vehicle doesn't fire the EH for each hitpoint so the "ace_hdbracket" code never runs
// It does fire the EH multiple times, but this seems to scale with the intensity of the crash
private _vehicle = vehicle _unit;
if (
EGVAR(medical,enableVehicleCrashes) &&
{_hitPoint isEqualTo "#structural"} &&
{_ammo isEqualTo ""} &&
{_vehicle != _unit} &&
{vectorMagnitude (velocity _vehicle) > 5}
// todo: no way to detect if stationary and another vehicle hits you
) exitWith {
TRACE_5("Crash",_unit,_shooter,_instigator,_damage,_newDamage);
[QEGVAR(medical,woundReceived), [_unit, [[_newDamage, _hitPoint, _newDamage]], _unit, "vehiclecrash"]] call CBA_fnc_localEvent;
0
};
// This hitpoint is set to trigger last, evaluate all the stored damage values
// to determine where wounds are applied
@ -52,7 +73,7 @@ if (_hitPoint isEqualTo "ace_hdbracket") exitWith {
_unit setVariable [QEGVAR(medical,lastDamageSource), _shooter];
_unit setVariable [QEGVAR(medical,lastInstigator), _instigator];
private _damageStructural = _unit getVariable [HIT_STRUCTURAL, 0];
private _damageStructural = _unit getVariable [QGVAR($#structural), [0,0]];
// --- Head
private _damageHead = [
@ -81,38 +102,20 @@ if (_hitPoint isEqualTo "ace_hdbracket") exitWith {
private _damageRightLeg = _unit getVariable [QGVAR($HitRightLeg), [0,0]];
// Find hit point that received the maxium damage
// Priority used for sorting if incoming damage is equivalent (e.g. max which is 4)
// Priority used for sorting if incoming damage is equal
private _allDamages = [
_damageHead + [PRIORITY_HEAD, "Head"],
_damageBody + [PRIORITY_BODY, "Body"],
_damageLeftArm + [PRIORITY_LEFT_ARM, "LeftArm"],
_damageRightArm + [PRIORITY_RIGHT_ARM, "RightArm"],
_damageLeftLeg + [PRIORITY_LEFT_LEG, "LeftLeg"],
_damageRightLeg + [PRIORITY_RIGHT_LEG, "RightLeg"]
[_damageHead select 0, PRIORITY_HEAD, _damageHead select 1, "Head"],
[_damageBody select 0, PRIORITY_BODY, _damageBody select 1, "Body"],
[_damageLeftArm select 0, PRIORITY_LEFT_ARM, _damageLeftArm select 1, "LeftArm"],
[_damageRightArm select 0, PRIORITY_RIGHT_ARM, _damageRightArm select 1, "RightArm"],
[_damageLeftLeg select 0, PRIORITY_LEFT_LEG, _damageLeftLeg select 1, "LeftLeg"],
[_damageRightLeg select 0, PRIORITY_RIGHT_LEG, _damageRightLeg select 1, "RightLeg"],
[_damageStructural select 0, PRIORITY_STRUCTURAL, _damageStructural select 1, "#structural"]
];
TRACE_2("incoming",_allDamages,_damageStructural);
// represents all incoming damage for selecting a non-selectionSpecific wound location, (used for selectRandomWeighted [value1,weight1,value2....])
private _damageSelectionArray = [
HITPOINT_INDEX_HEAD, _damageHead select 1, HITPOINT_INDEX_BODY, _damageBody select 1, HITPOINT_INDEX_LARM, _damageLeftArm select 1,
HITPOINT_INDEX_RARM, _damageRightArm select 1, HITPOINT_INDEX_LLEG, _damageLeftLeg select 1, HITPOINT_INDEX_RLEG, _damageRightLeg select 1
];
_allDamages sort false;
(_allDamages select 0) params ["", "_receivedDamage", "", "_woundedHitPoint"];
private _receivedDamageHead = _damageHead select 1;
if (_receivedDamageHead >= HEAD_DAMAGE_THRESHOLD) then {
TRACE_3("reporting fatal head damage instead of max",_receivedDamageHead,_receivedDamage,_woundedHitPoint);
_receivedDamage = _receivedDamageHead;
_woundedHitPoint = "Head";
};
// We know it's structural when no specific hitpoint is damaged
if (_receivedDamage == 0) then {
_receivedDamage = _damageStructural select 1;
_woundedHitPoint = "Body";
_damageSelectionArray = [1, 1]; // sum of weights would be 0
};
_allDamages = _allDamages apply {[_x select 2, _x select 3, _x select 0]};
// Environmental damage sources all have empty ammo string
// No explicit source given, we infer from differences between them
@ -120,8 +123,6 @@ if (_hitPoint isEqualTo "ace_hdbracket") exitWith {
// Any collision with terrain/vehicle/object has a shooter
// Check this first because burning can happen at any velocity
if !(isNull _shooter) then {
_ammo = "collision"; // non-selectionSpecific so only _damageSelectionArray matters
/*
If shooter != unit then they hit unit, otherwise it could be:
- Unit hitting anything at speed
@ -130,48 +131,24 @@ if (_hitPoint isEqualTo "ace_hdbracket") exitWith {
Assume fall damage for downward velocity because it's most common
*/
if (_shooter == _unit && {(velocity _unit select 2) < -2}) then {
_ammo = "falling"; // non-selectionSpecific so only _damageSelectionArray matters
_damageSelectionArray = [HITPOINT_INDEX_RLEG, 1, HITPOINT_INDEX_LLEG, 1];
TRACE_5("Fall",_unit,_shooter,_instigator,_damage,_receivedDamage);
_ammo = "falling";
TRACE_5("Fall",_unit,_shooter,_instigator,_damage,_allDamages);
} else {
_damageSelectionArray = [HITPOINT_INDEX_RARM, 1, HITPOINT_INDEX_LARM, 1, HITPOINT_INDEX_LLEG, 1, HITPOINT_INDEX_RLEG, 1];
TRACE_5("Collision",_unit,_shooter,_instigator,_damage,_receivedDamage);
};
// Significant damage suggests high relative velocity
// Momentum transfers to body/head for worse wounding
// Higher momentum results in higher chance for head to be hit for more lethality
if (_receivedDamage > 0.35) then {
private _headHitWeight = (_receivedDamage / 2) min 1;
if (_receivedDamage < 0.6) then {
_damageSelectionArray append [0, (1 - _headHitWeight), 1, _headHitWeight];
} else {
_damageSelectionArray = [0, (1 - _headHitWeight), 1, _headHitWeight];
}
_ammo = "collision";
TRACE_5("Collision",_unit,_shooter,_instigator,_damage,_allDamages);
};
} else {
// Anything else is almost guaranteed to be fire damage
_damageSelectionArray = [HITPOINT_INDEX_BODY, 1, HITPOINT_INDEX_LLEG, 1, HITPOINT_INDEX_RLEG, 1];;
_ammo = "unknown"; // non-selectionSpecific so only _damageSelectionArray matters
// Fire damage can occur as lots of minor damage events
// Combine these until significant enough to wound
private _combinedDamage = _receivedDamage + (_unit getVariable [QGVAR(trivialDamage), 0]);
if (_combinedDamage > 0.1) then {
_unit setVariable [QGVAR(trivialDamage), 0];
_receivedDamage = _combinedDamage;
TRACE_5("Burning",_unit,_shooter,_instigator,_damage,_receivedDamage);
} else {
_unit setVariable [QGVAR(trivialDamage), _combinedDamage];
_receivedDamage = 0;
};
_ammo = "fire";
TRACE_5("Fire Damage",_unit,_shooter,_instigator,_damage,_allDamages);
};
};
// No wounds for minor damage
if (_receivedDamage > 1E-3) then {
TRACE_3("received",_receivedDamage,_woundedHitPoint,_damageSelectionArray);
[QEGVAR(medical,woundReceived), [_unit, _woundedHitPoint, _receivedDamage, _shooter, _ammo, _damageSelectionArray]] call CBA_fnc_localEvent;
// TODO check if this needs to be changed for burning damage (occurs as lots of small events that we add together)
if ((_allDamages select 0 select 0) > 1E-3) then {
TRACE_1("received",_allDamages);
[QEGVAR(medical,woundReceived), [_unit, _allDamages, _shooter, _ammo]] call CBA_fnc_localEvent;
};
// Clear stored damages otherwise they will influence future damage events
@ -181,45 +158,19 @@ if (_hitPoint isEqualTo "ace_hdbracket") exitWith {
} forEach [
QGVAR($HitFace),QGVAR($HitNeck),QGVAR($HitHead),
QGVAR($HitPelvis),QGVAR($HitAbdomen),QGVAR($HitDiaphragm),QGVAR($HitChest),QGVAR($HitBody),
QGVAR($HitLeftArm),QGVAR($HitRightArm),QGVAR($HitLeftLeg),QGVAR($HitRightLeg)
QGVAR($HitLeftArm),QGVAR($HitRightArm),QGVAR($HitLeftLeg),QGVAR($HitRightLeg),
QGVAR($#structural)
];
0
};
// Drowning doesn't fire the EH for each hitpoint so the "ace_hdbracket" code never runs
// Damage occurs in consistent increments
if (
_hitPoint isEqualTo "#structural" &&
{getOxygenRemaining _unit <= 0.5} &&
{_damage isEqualTo (_oldDamage + 0.005)}
) exitWith {
[QEGVAR(medical,woundReceived), [_unit, "Body", _newDamage, _unit, "drowning", [HITPOINT_INDEX_BODY, 1]]] call CBA_fnc_localEvent;
TRACE_5("Drowning",_unit,_shooter,_instigator,_damage,_newDamage);
// Damages are stored for "ace_hdbracket" event triggered last
_unit setVariable [format [QGVAR($%1), _hitPoint], [_realDamage, _newDamage]];
0
};
// Crashing a vehicle doesn't fire the EH for each hitpoint so the "ace_hdbracket" code never runs
// It does fire the EH multiple times, but this seems to scale with the intensity of the crash
private _vehicle = vehicle _unit;
if (
EGVAR(medical,enableVehicleCrashes) &&
{_hitPoint isEqualTo "#structural"} &&
{_ammo isEqualTo ""} &&
{_vehicle != _unit} &&
{vectorMagnitude (velocity _vehicle) > 5}
// todo: no way to detect if stationary and another vehicle hits you
) exitWith {
private _damageSelectionArray = [
HITPOINT_INDEX_HEAD, 1, HITPOINT_INDEX_BODY, 1, HITPOINT_INDEX_LARM, 1,
HITPOINT_INDEX_RARM, 1, HITPOINT_INDEX_LLEG, 1, HITPOINT_INDEX_RLEG, 1
];
[QEGVAR(medical,woundReceived), [_unit, "Body", _newDamage, _unit, "vehiclecrash", _damageSelectionArray]] call CBA_fnc_localEvent;
TRACE_5("Crash",_unit,_shooter,_instigator,_damage,_newDamage);
0
};
// Engine damage to these hitpoints controls blood visuals, limping, weapon sway
// Handled in fnc_damageBodyPart, persist here
if (_hitPoint in ["hithead", "hitbody", "hithands", "hitlegs"]) exitWith {_oldDamage};
// We store our own damage values so engine damage is unnecessary
0

View File

@ -55,7 +55,7 @@ if (!_isLimping && {EGVAR(medical,limping) > 0}) then {
{
_x params ["_xClassID", "_xBodyPartN", "_xAmountOf", "", "_xDamage"];
if ((_xBodyPartN > 3) && {_xAmountOf > 0} && {_xDamage > LIMPING_DAMAGE_THRESHOLD} && {
(EGVAR(medical_damage,woundsData) select (_xClassID / 10)) select 7}) exitWith { // select _causeLimping from woundsData
(EGVAR(medical_damage,woundDetails) get (_xClassID / 10)) select 3}) exitWith { // select _causeLimping from woundDetails
TRACE_1("limping because of wound",_x);
_isLimping = true;
};

View File

@ -32,6 +32,7 @@
#define PRIORITY_RIGHT_ARM (1 + random 1)
#define PRIORITY_LEFT_LEG (1 + random 1)
#define PRIORITY_RIGHT_LEG (1 + random 1)
#define PRIORITY_STRUCTURAL 1
// don't change, these reflect hard coded engine behaviour
#define DAMAGED_MIN_THRESHOLD 0.45

View File

@ -7,16 +7,19 @@
class My_AwesomeUnit_base;
class My_AwesomeUnit: My_AwesomeUnit_base {
class HitPoints {
ADD_ACE_HITPOINTS(2,2);
class HitHands;
class HitLegs;
ADD_ACE_HITPOINTS;
};
};
};
*/
// Our method for adding left and right arm and leg armor. Uses those selections
// that are used for animations and therefore exist in all third party units
// usage: arm_armor and leg_armor are the armor values of of HitHands and
// HitLegs respectively.
// that are used for animations and therefore exist in all third party units.
// This used to take the armor values as parameters; it now inherits the values
// of `armor`, `passThrough` and `explosionShielding` from the existing hitpoints
// for vanilla consistency.
// "ACE_HDBracket" is a special hit point. It is designed in a way where the
// "HandleDamage" event handler will compute it at the end of every damage
// calculation step. This way we can figure out which hit point took the most
@ -24,27 +27,21 @@
// the hit point itself should not take any damage
// It is important that the "ACE_HDBracket" hit point is the last in the config,
// but has the same selection as the first one (always "HitHead" for soldiers).
#define ADD_ACE_HITPOINTS(arm_armor,leg_armor)\
class HitLeftArm {\
armor = arm_armor;\
#define ADD_ACE_HITPOINTS\
class HitLeftArm: HitHands {\
material = -1;\
name = "hand_l";\
passThrough = 1;\
radius = 0.08;\
explosionShielding = 1;\
visual = "injury_hands";\
minimalHit = 0.01;\
};\
class HitRightArm: HitLeftArm {\
name = "hand_r";\
};\
class HitLeftLeg {\
armor = leg_armor;\
class HitLeftLeg: HitLegs {\
material = -1;\
name = "leg_l";\
passThrough = 1;\
radius = 0.1;\
explosionShielding = 1;\
visual = "injury_legs";\
minimalHit = 0.01;\
};\

View File

@ -4,7 +4,7 @@
#define ALL_BODY_PARTS ["head", "body", "leftarm", "rightarm", "leftleg", "rightleg"]
#define ALL_SELECTIONS ["head", "body", "hand_l", "hand_r", "leg_l", "leg_r"]
#define ALL_HITPOINTS ["HitHead", "HitBody", "HitLeftArm", "HitRightArm", "HitLeftLeg", "HitRightLeg"]
#define ALL_HITPOINTS ["HitHead", "HitChest", "HitLeftArm", "HitRightArm", "HitLeftLeg", "HitRightLeg"]
#define HITPOINT_INDEX_HEAD 0
#define HITPOINT_INDEX_BODY 1

View File

@ -114,7 +114,7 @@ private _fnc_processWounds = {
private _classIndex = _woundClassID / 10;
private _category = _woundClassID % 10;
private _className = EGVAR(medical_damage,woundsData) select _classIndex select 6;
private _className = EGVAR(medical_damage,woundClassNames) select _classIndex;
private _suffix = ["Minor", "Medium", "Large"] select _category;
private _woundName = localize format [ELSTRING(medical_damage,%1_%2), _className, _suffix];

View File

@ -0,0 +1,228 @@
---
layout: wiki
title: Medical Framework
description: This document explains how to extend the ACE Medical Framework to modify or add functionality.
group: framework
order: 5
parent: wiki
mod: ace
version:
major: 3
minor: 13
patch: 7
---
## 1. Overview
ACE Medical is a large system with a lot of functionality spread across multiple components. Some of the system is hard-coded, but a lot of it is defined through config so it can be altered or extended without causing problems.
## 2. Processing Flow
This section explains the components responsible for turning damage into wounds, and how they interact with each other.
### 2.1 Damage Handling
ACE Medical Engine uses the `handleDamage` event handler to intercept incoming damage and replace it with wounds. This function is complex as it must work around a number of quirks in the engine and the vanilla game's handling of damage, so it should generally be left alone, but the important note is that if another mod or script adds its own `handleDamage` event handler this is virtually guaranteed to break ACE's handling and cause undesired behaviour.
This event handler aggregates incoming damage information into individual damage events, then uses the CBA event system to pass on these individual damage events. For each thing that damages a unit (each bullet or explosion) it raises an event of type `ace_medical_woundReceived`. A couple of minor exceptions to this are burning (e.g. from being near a burning vehicle) and some types of collisions, which create a large number of small damage ticks because the event handler doesn't provide enough information to collect them into one event.
#### 2.1.1 Damage Type
The `woundReceived` event receives information about how much damage was dealt to each body part and the ammo type - each `CfgAmmo` [class](#41-cfgammo) should define an `ACE_damageType` property which the handler uses to determine what type of damage that ammo does. ACE Medical Damage sets this property on all relevant base classes, so if your custom ammo class inherits from any vanilla ammo it will also inherit the corresponding damage type. Usually this is what you want, though sometimes you may want to change it e.g. a special type of bullet that actually deals `explosive` damage.
The default `woundReceived` event handler uses the `ace_medical_damage_fnc_getTypeOfDamage` function to look up and cache the damage type. Some types of damage, such as burning, collisions and falls, aren't caused by weapons therefore don't have an ammo type - instead they pass the name of the damage type. All the configured damage types are preloaded into the cache, so it simply returns the same string.
#### 2.1.2 Selection Specific
Due to a quirk in the engine behaviour, projectiles like bullets don't only deal damage to the body part they hit - they also deal damage to other nearby parts. To get around this, ACE treats certain [damage types](#43-acemedicalinjuries--damagetypes) (like `bullet`) as "selection-specific", meaning that when damage of that type generates wounds they will only be applied to whichever body part took the _most_ damage, which should be whichever part was _actually_ hit.
Damage types which are non-selection-specific create wounds on every body part that received damage (which is often all of them).
The `woundReceived` event is passed all the damage and the default event handler added by ACE Medical Damage checks the damage type config to decide whether to pass on all of the damage, or just one part.
### 2.2 Wound Handlers
To convert these raw damage numbers into discrete wounds, ACE uses a collection of functions called wound handlers. Each [damage type](#43-acemedicalinjuries--damagetypes) may specify its own list of wound handlers under a `woundHandlers` class, or if it does not the default handlers defined in `ACE_Medical_Injuries >> "damageTypes" >> "woundHandlers"` will be used.
All wound handlers defined for a particular damage type will be called, and they are called in the _opposite_ order they were added. The value returned by each handler is what will be passed as parameters to the next one - if the return is not valid (not an array, or not enough arguments) handling will be terminated and no other handler functions will be called. This allows custom wound handlers to selectively handle damage and either block the built-in handling or pass modified damage data.
ACE comes with a built-in wound handler that does all the work of creating wounds, checking for death or unconsciousness and updating important effects (like the blood visuals that appear on body parts). ACE also uses some custom wound handlers which usually perform additional processing and then pass the result on to be used by the default handler - a good example of this is `ace_medical_damage_fnc_woundsVehicleCrash`. If you need to add custom handling for a damage type it is recommended to use this pattern to avoid breaking existing functionality; if you choose not to pass damage on to the default handler, you must ensure that all the necessary processing is done.
## 3. Extending ACE Medical
### 3.1 Adding new ammo types
Generally when adding new ammo types it is sufficient to simply inherit from an existing vanilla class; your ammo will inherit the damage type of its parent class. You can check this using the in-game config viewer.
If, for whatever reason, you want to change the damage type you can simply define the `ACE_damageType` [property](#41-cfgammo).
### 3.2 Adding new wound types
To add a new wound type all that is technically necessary is to create a new [wound class](#42-acemedicalinjuries--wounds).
In order to allow that wound type to be created, you must add it to one or more [damage types](#43-acemedicalinjuries--damagetypes) or create new damage types that cause your new wound type.
To allow your wound to be bandaged or treated in other ways, look into the [ACE Medical Treatment](https://github.com/acemod/ACE3/tree/master/addons/medical_treatment) component (documentation not yet available).
### 3.3 Adding new damage types
To create a new damage type, create a new [damage type class](#43-acemedicalinjuries--damagetypes) and define a class within it for each type of wound you want it to be able to create. All the properties listed are optional and most can be left at their default values or tweaked to produce special behaviour, but you will probably want to change the `thresholds` and `weighting`.
If you want to add custom wound handling, define the `woundHandlers` class and create a [handler function](#44-wound-handler-function). Your `woundHandlers` class may inherit from the default one to automatically add the default wound handlers to it.
#### 3.3.1 Thresholds
This entry is used to determine how many individual wounds that type of damage will create. Its format is:
```cpp
class MY_custom_damageType {
thresholds[] = {
{damage, number of wounds},
...
};
...
};
```
When damage of this type is dealt, the wound handler scans through this array to find the _first_ entry whose damage value is _less than_ the incoming damage; it then takes that entry and the one _before_ it in the list and uses `linearConversion` to interpolate between the two "number of wounds" values. If the result is a decimal, it is rounded up or down by chance based on the decimal part. Usually the points should be in descending order of `damage`.
For specific details, look at `ace_medical_damage_fnc_interpolatePoints`.
Example:
```cpp
thresholds[] = {{0.7, 3}, {0.5, 2}, {0.3, 1}};
```
- `damage = 0.65`
- 0.65 75% of the way from 0.5 to 0.7, so the interpolated value is 2.75
- this has a 75% chance to create 3 wounds, and a 25% chance to create 2 wounds
- the incoming damage is split evenly between the wounds created
Additionally:
- If the damage value is greater than the first entry in the list, the first number is returned as-is. In the above example, a damage value of 0.8 would always create 3 wounds.
- If the damage value is lower than the last entry in the list, that value is used as-is. In the example, any value below 0.3 would always create 1 wound.
#### 3.3.2 Weighting
This entry is used to determine the chance for a damage type to produce each wound type. Its format is the same as thresholds and it is interpolated in the same way, except that decimals are **not rounded** and instead returned as-is. This is calculated for each wound type and the results are used as weights for `selectRandomWeighted` to determine which wound is created.
- The weights are calculated once per body part damaged.
- If multiple wounds are created, the type is chosen randomly for each one but the weighting remains the same.
- The damage value used is the damage _per wound_, which is the total damage divided by the number of wounds.
Example:
```cpp
class Contusion {
weighting[] = {{0.35, 0}, {0, 2}};
};
class VelocityWound {
weighting[] = {{0.35, 2}, {0, 0}};
};
```
- `damage = 0.6` and `nWounds = 3`, therefore `damagePerWound = 0.2`
- 0.2 is 4/7 of the way from 0 to 0.35
- `weight` for contusion is approximately `0.857`
- `weight` for velocitywound is approximately `1.143`
- total `weight` is `2`
- this means each wound has about a 43% chance to be a contusion, and 57% to be a velocity wound
## 4. Reference
### 4.1 CfgAmmo
```cpp
class CfgAmmo {
class BulletCore;
class BulletBase: BulletCore {
ACE_damageType = "bullet"; //any valid damage type
};
class B_20mm: BulletBase {
ACE_damageType = "explosive"; //sub-classes are free to override
};
};
```
### 4.2 ACE_Medical_Injuries >> wounds
```cpp
class ACE_Medical_Injuries {
class wounds {
// each sub-class defines a valid wound type
class MY_custom_woundType {
bleeding = 0.05; // maximum blood loss per wound as a multiple of cardiac output, will be scaled by wound size. (default: 0)
pain = 0.8; // maximum pain produced on a scale of 0..1, will be scaled by wound size (default: 0)
causeLimping = 1; // 0 to ignore this wound type when determining whether damage to the legs is sufficient to cause limping (default: 0)
causeFracture = 1; // 0 to prevent this wound type from causing fractures (default: 0)
};
};
};
```
### 4.3 ACE_Medical_Injuries >> damageTypes
See above for full explanations of [thresholds](#331-thresholds) and [weighting](#332-weighting).
```cpp
class ACE_Medical_Injuries {
class damageTypes {
// default values used if a damage type does not define them itself
thresholds[] = {{0.1, 1}};
selectionSpecific = 1;
// list of damage handlers, which will be called in reverse order
// each entry should be a SQF expression that returns a function
// this can also be overridden for each damage type
class woundHandlers {
ace_medical_damage = "ace_medical_damage_fnc_woundsHandlerActive";
};
// each sub-class defines a valid damage type
class MY_custom_damageType {
// this is used to determine how many wounds to produce - see explanation above
thresholds[] = {{0.1, 1}, {0.1, 0}};
// if 1, wounds are only applied to the most-damaged body part. if 0, wounds are applied to all damaged parts
selectionSpecific = 1;
// custom handling for this damage type
// inherits from the default handlers - the function(s) defined here will be called first, then the default one(s)
class woundHandlers: woundHandlers {
my_addon = "my_fnc_customDamageHandler";
};
// one class for each type of wound this damage type is allowed to create
// must match a wound type defined above
class MY_custom_woundType {
// used to determine the chance of producing this type of wound instead of another - see explanation above
weighting[] = {{0.01, 1}, {0.01, 0}};
// multiplier for incoming damage, applied before anything else is calculated (default: 1)
damageMultiplier = 1;
// multiplies the damage value used to determine wound size as shown in the UI.
// size is used to scale bleeding & pain but *not* death or unconsciousness (default: 1)
sizeMultiplier = 1;
// multiplies bleeding rate (applied after size) (default: 1)
bleedingMultiplier = 1;
// multiplies pain produced (applied after size) (default: 1)
painMultiplier = 1;
// multiplies the probability to create fractures (default: 1)
fractureMultiplier = 1;
};
class Contusion {
weighting[] = {{0.35, 0}, {0.35, 1}};
};
class VelocityWound {
weighting[] = {{0.35, 1}, {0.35, 0}};
};
};
};
};
```
## 4.4 Wound Handler Function
Custom wound handlers should follow the same spec as the built-in handler:
`ace_medical_damage_fnc_woundsHandlerSQF`
| Arguments | Type | Optional (default value)
---| --------- | ---- | ------------------------
0 | Unit that was hit | Object | Required
1 | Array of damage dealt to each body part | Array | Required
2 | Type of damage | String | Required
**R** | Parameters to be passed to the next handler in the list, e.g. `_this` or a modified copy of it. Return `[]` to prevent further handling. | Array | Required
The damage elements are sorted in descending order according to how much damage was dealt to each body part _before armor was taken into account_, but the actual damage values are _after armor_.
### Example
`[player, [[0.5, "Body", 1], [0.3, "Head", 0.6]], "grenade"] ace_medical_damage_fnc_woundsHandlerSQF`
| Arguments | Explanation
---| --------- | -----------
0 | `player` | Unit that was hit
1 | `[[0.5, "Body", 1], [0.3, "Head", 0.6]]` | 0.5 damage to body (was 1 before armor), 0.3 damage to head (was 0.6 before armor)
2 | `"grenade"` | type grenade (non-selection-specific)