Add defend wave templates and standardize task endings

- Let defend tasks use synced enemy groups as wave templates
- Record task status changes on fail/success
- Route end conditions through the server-side mission end helper
This commit is contained in:
Jacob Schmidt 2026-05-14 19:20:44 -05:00
parent 9be7f91e2f
commit e6eceac4ec
21 changed files with 211 additions and 97 deletions

View File

@ -375,13 +375,13 @@ class CfgVehicles {
};
class ModuleDescription: ModuleDescription {
description = "Creates a defend task with configurable defense zone and enemy wave parameters";
sync[] = { "Anything" };
description = "Creates a defend task with configurable defense zone and designer-controlled enemy wave templates";
sync[] = { "AnyBrain" };
class Anything {
class AnyBrain {
description[] = {
"Defend task module",
"Enemy waves are spawned automatically; no synced entities are required"
"Sync with enemy units or group members to use their groups as wave templates"
};
position = 1;
direction = 1;

View File

@ -143,7 +143,8 @@ Available task modules:
- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and
`FORGE_Module_Shooters`
- `FORGE_Module_HVT`: sync directly to HVT units
- `FORGE_Module_Defend`: configure the defense marker and wave settings
- `FORGE_Module_Defend`: configure the defense marker and wave settings; sync
enemy units to use their groups as wave templates
These modules delegate to `fnc_startTask.sqf`.

View File

@ -119,7 +119,7 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
["INFO", format [
"Attack task %1 succeeded. TargetsRequired=%2, TargetsKilled=%3",
@ -149,5 +149,5 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};

View File

@ -16,11 +16,12 @@
* 8: Enemy wave count <NUMBER> (default: 3)
* 9: Time between waves in seconds <NUMBER> (default: 300)
* 10: Minimum BLUFOR units required in zone <NUMBER> (default: 1)
* 11: Equipment rewards <ARRAY> (default: [])
* 12: Supply rewards <ARRAY> (default: [])
* 13: Weapon rewards <ARRAY> (default: [])
* 14: Vehicle rewards <ARRAY> (default: [])
* 15: Special rewards <ARRAY> (default: [])
* 11: Enemy template groups <ARRAY> (default: [])
* 12: Equipment rewards <ARRAY> (default: [])
* 13: Supply rewards <ARRAY> (default: [])
* 14: Weapon rewards <ARRAY> (default: [])
* 15: Vehicle rewards <ARRAY> (default: [])
* 16: Special rewards <ARRAY> (default: [])
*
* Return Value:
* None
@ -43,6 +44,7 @@ params [
["_waveCount", 3, [0]],
["_waveCooldown", 300, [0]],
["_minBlufor", 1, [0]],
["_enemyTemplates", [], [[]]],
["_equipmentRewards", [], [[]]],
["_supplyRewards", [], [[]]],
["_weaponRewards", [], [[]]],
@ -104,7 +106,7 @@ waitUntil {
};
if (_currentWave < _waveCount && _defenseStarted && { time >= _nextWaveTime }) then {
[_defenseZone, _taskID, _currentWave] call FUNC(spawnEnemyWave);
[_defenseZone, _taskID, _currentWave, _enemyTemplates] call FUNC(spawnEnemyWave);
_currentWave = _currentWave + 1;
_nextWaveTime = time + _waveCooldown;
@ -119,6 +121,7 @@ waitUntil {
if (_result == 1) then {
[_taskID, "FAILED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
sleep 1;
@ -126,7 +129,7 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
private _rewards = createHashMap;
_rewards set ["funds", _companyFunds];
@ -139,6 +142,7 @@ if (_result == 1) then {
[_taskID, _rewards] call FUNC(handleTaskRewards);
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
sleep 1;
@ -146,5 +150,5 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};

View File

@ -84,13 +84,14 @@ if (_result == 1) then {
{ deleteVehicle _x } forEach _entities;
[_taskID, "FAILED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
sleep 1;
GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
{ deleteVehicle _x } forEach _ieds;
{ deleteVehicle _x } forEach _entities;
@ -106,13 +107,14 @@ if (_result == 1) then {
[_taskID, _rewards] call FUNC(handleTaskRewards);
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
sleep 1;
GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};
GVAR(TaskStore) call ["clearTask", [_taskID]];

View File

@ -95,6 +95,7 @@ if (_result == 1) then {
{ deleteVehicle _x } forEach _cargo;
[_taskID, "FAILED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
sleep 1;
@ -102,7 +103,7 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
{ deleteVehicle _x } forEach _cargo;
@ -117,6 +118,7 @@ if (_result == 1) then {
[_taskID, _rewards] call FUNC(handleTaskRewards);
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
sleep 1;
@ -124,5 +126,5 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};

View File

@ -89,6 +89,7 @@ if (_result == 1) then {
{ deleteVehicle _x } forEach _targets;
[_taskID, "FAILED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
sleep 1;
@ -96,7 +97,7 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
{ deleteVehicle _x } forEach _targets;
@ -111,6 +112,7 @@ if (_result == 1) then {
[_taskID, _rewards] call FUNC(handleTaskRewards);
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
sleep 1;
@ -118,5 +120,5 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};

View File

@ -91,29 +91,27 @@ waitUntil {
sleep 1;
GVAR(TaskStore) call ["trackParticipants", [_taskID, _hostages + _shooters, _extZone, 250]];
private _playerGroups = allPlayers apply { group _x };
private _hostagesFreed = ({
alive _x && { ((group _x) in _playerGroups) || { !captive _x } }
} count _hostages);
private _hostagesInZone = ({ _x inArea _extZone } count _hostages);
private _hostagesKilled = ({ !alive _x } count _hostages);
private _shootersAlive = ({ alive _x } count _shooters);
private _hostageSucceeded = (_hostagesInZone >= _requiredRescues) && { _hostagesKilled < _maxHostageLosses };
private _shootersClearedSucceeded = (!isNil "_shooters") && { _shootersAlive <= 0 } && { _hostageSucceeded };
if (_timeLimit isNotEqualTo 0) then {
private _timeExpired = (floor time - _startTime >= _timeLimit);
if (_hostagesFreed < _requiredRescues && _timeExpired) then { _result = 1; };
if (!_hostageSucceeded && _timeExpired) then { _result = 1; };
if (_hostagesKilled >= _maxHostageLosses) then { _result = 1; };
(_result == 1) or
((_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses)) or
((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses))
_hostageSucceeded or
_shootersClearedSucceeded
} else {
if (_hostagesKilled >= _maxHostageLosses) then { _result = 1; };
(_result == 1) or
((_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses)) or
((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses))
_hostageSucceeded or
_shootersClearedSucceeded
};
};
@ -152,6 +150,7 @@ if (_result == 1) then {
{ deleteVehicle _x } forEach _shooters;
[_taskID, "FAILED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
sleep 1;
@ -159,7 +158,7 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
{ deleteVehicle _x } forEach _hostages;
{ deleteVehicle _x } forEach _shooters;
@ -175,6 +174,7 @@ if (_result == 1) then {
[_taskID, _rewards] call FUNC(handleTaskRewards);
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
sleep 1;
@ -182,5 +182,5 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};

View File

@ -66,6 +66,8 @@ waitUntil {
};
_hvts = GVAR(TaskStore) call ["getTaskEntities", ["hvts", _taskID]];
private _requiredHvts = if (_limitSuccess < 0) then { count _hvts } else { _limitSuccess };
private _maxHvtLosses = if (_limitFail < 0) then { count _hvts } else { _limitFail };
if (_timeLimit isNotEqualTo 0) then {
waitUntil {
@ -80,22 +82,23 @@ waitUntil {
sleep 1;
GVAR(TaskStore) call ["trackParticipants", [_taskID, _hvts, _extZone, 250]];
private _hvtsCaptive = ({ captive _x } count _hvts);
private _hvtsKilled = ({ !alive _x } count _hvts);
private _hvtsInZone = ({ _x inArea _extZone } count _hvts);
private _captureSucceeded = _capture && { _hvtsInZone >= _requiredHvts } && { _hvtsKilled < _maxHvtLosses };
private _eliminateSucceeded = _eliminate && { _hvtsKilled >= _requiredHvts };
if (_timeLimit isNotEqualTo 0) then {
private _timeExpired = (floor time - _startTime >= _timeLimit);
if (_capture && _hvtsKilled >= _limitFail) then { _result = 1; };
if (_capture && _hvtsCaptive < _limitSuccess && _timeExpired) then { _result = 1; };
if (_eliminate && _hvtsKilled < _limitSuccess && _timeExpired) then { _result = 1; };
if (_capture && { _hvtsKilled >= _maxHvtLosses }) then { _result = 1; };
if (_capture && { !_captureSucceeded } && { _timeExpired }) then { _result = 1; };
if (_eliminate && { !_eliminateSucceeded } && { _timeExpired }) then { _result = 1; };
(_result == 1) or (_capture && (_hvtsInZone >= _limitSuccess) && (_hvtsKilled < _limitFail)) or (_eliminate && (_hvtsKilled >= _limitSuccess))
(_result == 1) or _captureSucceeded or _eliminateSucceeded
} else {
if (_capture && (_hvtsKilled >= _limitFail)) then { _result = 1; };
if (_capture && { _hvtsKilled >= _maxHvtLosses }) then { _result = 1; };
(_result == 1) or (_capture && (_hvtsInZone >= _limitSuccess) && (_hvtsKilled < _limitFail)) or (_eliminate && (_hvtsKilled >= _limitSuccess))
(_result == 1) or _captureSucceeded or _eliminateSucceeded
};
};
@ -103,6 +106,7 @@ if (_result == 1) then {
{ deleteVehicle _x } forEach _hvts;
[_taskID, "FAILED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
sleep 1;
@ -110,7 +114,7 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
{ deleteVehicle _x } forEach _hvts;
@ -125,6 +129,7 @@ if (_result == 1) then {
[_taskID, _rewards] call FUNC(handleTaskRewards);
[_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
sleep 1;
@ -132,5 +137,5 @@ if (_result == 1) then {
GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
GVAR(TaskStore) call ["clearTask", [_taskID]];
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};

View File

@ -28,8 +28,6 @@ SETVAR(_entity,assignedTask,_taskID);
GVAR(TaskStore) call ["registerTaskEntity", ["hostages", _taskID, _entity]];
if (alive _entity) then {
// Apply hostage protection immediately so nearby hostile AI cannot kill the
// unit before the scheduled heartbeat initializes the animation state.
_entity setCaptive true;
_entity enableAIFeature ["MOVE", false];

View File

@ -8,6 +8,7 @@
* 0: Defense zone marker name <STRING>
* 1: Task ID <STRING>
* 2: Wave number (0-based) <NUMBER>
* 3: Enemy template groups <ARRAY> (default: [])
*
* Return Value:
* None
@ -18,7 +19,7 @@
* Public: No
*/
params [["_defenseZone", "", [""]], ["_taskID", "", [""]], ["_waveNumber", 0, [0]]];
params [["_defenseZone", "", [""]], ["_taskID", "", [""]], ["_waveNumber", 0, [0]], ["_enemyTemplates", [], [[]]]];
if (_defenseZone == "") exitWith { ["ERROR", "No defense zone provided for enemy wave spawn"] call EFUNC(common,log); };
@ -47,37 +48,83 @@ for "_i" from 0 to 3 do {
private _spawnPos = [_spawnX, _spawnY, 0];
private _safePos = _spawnPos findEmptyPosition [0, 50, "O_Soldier_F"];
if (count _safePos > 0) then {
_spawnPositions pushBack _safePos;
};
if (count _safePos > 0) then { _spawnPositions pushBack _safePos; };
};
private _groups = [];
{
private _groupSize = ceil(_unitCount / (count _spawnPositions));
private _group = createGroup east;
_groups pushBack _group;
if (_spawnPositions isEqualTo []) exitWith {
["ERROR", format ["Defense wave %1 for task %2 could not find spawn positions", _waveNumber + 1, _taskID]] call EFUNC(common,log);
};
for "_i" from 1 to _groupSize do {
private _unitType = _basicTypes select (floor random count _basicTypes);
private _roll = random 1;
if (_enemyTemplates isNotEqualTo []) then {
private _groupCount = ((_waveNumber + 1) min 4) min (count _spawnPositions);
private _selectedSpawnPositions = +_spawnPositions;
_selectedSpawnPositions resize _groupCount;
if (_roll < _eliteChance) then {
_unitType = _eliteTypes select (floor random count _eliteTypes);
} else {
if (_roll < _specialChance) then {
_unitType = _specialTypes select (floor random count _specialTypes);
{
private _spawnPos = _x;
private _templateGroup = selectRandom _enemyTemplates;
if !(_templateGroup isEqualType []) then { continue; };
if (_templateGroup isEqualTo []) then { continue; };
private _firstTemplate = _templateGroup select 0;
if !(_firstTemplate isEqualType createHashMap) then { continue; };
private _side = _firstTemplate getOrDefault ["side", east];
private _group = createGroup _side;
_groups pushBack _group;
{
private _unitTemplate = _x;
if !(_unitTemplate isEqualType createHashMap) then { continue; };
private _unitType = _unitTemplate getOrDefault ["type", "O_Soldier_F"];
private _unit = _group createUnit [_unitType, _spawnPos, [], 0, "NONE"];
_unit setVariable ["assignedTask", _taskID, true];
_unit setUnitLoadout (_unitTemplate getOrDefault ["loadout", getUnitLoadout _unit]);
_unit setSkill (_unitTemplate getOrDefault ["skill", skill _unit]);
_unit setRank (_unitTemplate getOrDefault ["rank", rank _unit]);
_unit setBehaviour "AWARE";
_unit setSpeedMode "NORMAL";
_unit enableDynamicSimulation true;
} forEach _templateGroup;
[_group, _center, _radius * 0.75] call CFUNC(taskDefend);
} forEach _selectedSpawnPositions;
["INFO", format [
"Spawned defense wave %1 for task %2 from %3 template group(s)",
_waveNumber + 1,
_taskID,
count _groups
]] call EFUNC(common,log);
} else {
{
private _groupSize = ceil(_unitCount / (count _spawnPositions));
private _group = createGroup east;
_groups pushBack _group;
for "_i" from 1 to _groupSize do {
private _unitType = _basicTypes select (floor random count _basicTypes);
private _roll = random 1;
if (_roll < _eliteChance) then {
_unitType = _eliteTypes select (floor random count _eliteTypes);
} else {
if (_roll < _specialChance) then {
_unitType = _specialTypes select (floor random count _specialTypes);
};
};
private _unit = _group createUnit [_unitType, _x, [], 0, "NONE"];
_unit setVariable ["assignedTask", _taskID, true];
_unit setBehaviour "AWARE";
_unit setSpeedMode "NORMAL";
_unit enableDynamicSimulation true;
};
private _unit = _group createUnit [_unitType, _x, [], 0, "NONE"];
_unit setVariable ["assignedTask", _taskID, true];
_unit setBehaviour "AWARE";
_unit setSpeedMode "NORMAL";
_unit enableDynamicSimulation true;
};
[_group, _center, _radius * 0.75] call CFUNC(taskDefend);
} forEach _spawnPositions;
[_group, _center, _radius * 0.75] call CFUNC(taskDefend);
} forEach _spawnPositions;
["INFO", format ["Spawned defense wave %1 for task %2 with %3 units", _waveNumber + 1, _taskID, _unitCount]] call EFUNC(common,log);
["INFO", format ["Spawned defense wave %1 for task %2 with %3 fallback units", _waveNumber + 1, _taskID, _unitCount]] call EFUNC(common,log);
};

View File

@ -47,6 +47,7 @@
* "waveCount" <NUMBER> (default: 3)
* "waveCooldown" <NUMBER> (default: 300)
* "minBlufor" <NUMBER> (default: 1)
* "enemyTemplates" <ARRAY> (default: [])
* 7: Minimum org reputation required <NUMBER> (default: 0)
* 8: Requester UID <STRING> (default: "")
* 9: Source tag <STRING> (default: "eden") -- "eden"|"mission_manager"|"script"
@ -186,7 +187,8 @@ private _handlerArgs = switch (_taskType) do {
private _waveCount = _taskParams getOrDefault ["waveCount", 3];
private _waveCooldown = _taskParams getOrDefault ["waveCooldown", 300];
private _minBlufor = _taskParams getOrDefault ["minBlufor", 1];
[_taskID, _defenseZone, _defendTime, _funds, _ratingFail, _ratingSuccess, _endSuccess, _endFail, _waveCount, _waveCooldown, _minBlufor] + _rewardTail
private _enemyTemplates = _taskParams getOrDefault ["enemyTemplates", []];
[_taskID, _defenseZone, _defendTime, _funds, _ratingFail, _ratingSuccess, _endSuccess, _endFail, _waveCount, _waveCooldown, _minBlufor, _enemyTemplates] + _rewardTail
};
default {
["ERROR", format ["startTask: unknown task type '%1'.", _taskType]] call EFUNC(common,log);

View File

@ -5,8 +5,7 @@
* Initializes the defend task module.
* Reads parameters from the logic object and delegates to fnc_startTask.
* The designer must place a named marker in Eden for the defense zone.
* Enemy waves are spawned automatically by fnc_defend — no entities need
* to be synced to this module.
* Synced enemy units are used as wave composition templates.
*
* Arguments:
* 0: Logic <OBJECT>
@ -33,13 +32,51 @@ if (_defenseZone isEqualTo "" || { markerShape _defenseZone isEqualTo "" }) exit
["ERROR", format ["Defend module '%1': DefenseZone marker '%2' is missing or invalid.", _taskID, _defenseZone]] call EFUNC(common,log);
};
private _syncedEnemies = synchronizedObjects _logic select { _x isKindOf "CAManBase" };
private _templateGroups = [];
private _templateUnits = [];
private _seenGroups = [];
{
private _group = group _x;
if (_group in _seenGroups) then { continue; };
_seenGroups pushBack _group;
private _templates = [];
{
if (isNull _x) then { continue; };
_templateUnits pushBackUnique _x;
_templates pushBack createHashMapFromArray [
["type", typeOf _x],
["loadout", getUnitLoadout _x],
["skill", skill _x],
["rank", rank _x],
["side", side _x]
];
} forEach (units _group);
if (_templates isNotEqualTo []) then {
_templateGroups pushBack _templates;
};
} forEach _syncedEnemies;
{ deleteVehicle _x } forEach _templateUnits;
if (_templateGroups isEqualTo []) then {
["WARNING", format [
"Defend module '%1' has no synced enemy units. Falling back to default CSAT wave templates.",
_taskID
]] call EFUNC(common,log);
};
["INFO", format [
"Defend Module Parameters: TaskID: %1, DefenseZone: %2, DefendTime: %3, WaveCount: %4, WaveCooldown: %5, MinBlufor: %6",
"Defend Module Parameters: TaskID: %1, DefenseZone: %2, DefendTime: %3, WaveCount: %4, WaveCooldown: %5, MinBlufor: %6, EnemyTemplateGroups: %7",
_taskID, _defenseZone,
_logic getVariable ["DefendTime", 600],
_logic getVariable ["WaveCount", 3],
_logic getVariable ["WaveCooldown", 300],
_logic getVariable ["MinBlufor", 1]
_logic getVariable ["MinBlufor", 1],
count _templateGroups
]] call EFUNC(common,log);
private _equipmentRewards = [_logic getVariable ["EquipmentRewards", "[]"], _taskID, "equipment"] call FUNC(parseRewards);
@ -66,6 +103,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID,
["waveCount", _logic getVariable ["WaveCount", 3]],
["waveCooldown", _logic getVariable ["WaveCooldown", 300]],
["minBlufor", _logic getVariable ["MinBlufor", 1]],
["enemyTemplates", _templateGroups],
["equipment", _equipmentRewards],
["supplies", _supplyRewards],
["weapons", _weaponRewards],

View File

@ -158,7 +158,7 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else {
{ deleteVehicle _x } forEach _targets;
@ -174,7 +174,7 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
};
_self call ["cleanup", []];

View File

@ -22,6 +22,7 @@ GVAR(DefendTaskBaseClass) = createHashMapFromArray [
_self set ["waveCount", _taskParams getOrDefault ["waveCount", 3]];
_self set ["waveCooldown", _taskParams getOrDefault ["waveCooldown", 300]];
_self set ["minBlufor", _taskParams getOrDefault ["minBlufor", 1]];
_self set ["enemyTemplates", _taskParams getOrDefault ["enemyTemplates", []]];
_self set ["nextWaveTime", -1];
_self set ["currentWave", 0];
_self set ["zoneEmptyCounter", 0];
@ -91,6 +92,7 @@ GVAR(DefendTaskBaseClass) = createHashMapFromArray [
private _waveCooldown = _self getOrDefault ["waveCooldown", 300];
private _minBlufor = _self getOrDefault ["minBlufor", 1];
private _currentWave = _self getOrDefault ["currentWave", 0];
private _enemyTemplates = _self getOrDefault ["enemyTemplates", []];
private _nextWaveTime = _self getOrDefault ["nextWaveTime", -1];
private _zoneEmptyCounter = _self getOrDefault ["zoneEmptyCounter", 0];
private _warningIssued = _self getOrDefault ["warningIssued", false];
@ -110,7 +112,7 @@ GVAR(DefendTaskBaseClass) = createHashMapFromArray [
};
if (_currentWave < _waveCount && { serverTime >= _nextWaveTime }) then {
[_defenseZone, _taskID, _currentWave] call FUNC(spawnEnemyWave);
[_defenseZone, _taskID, _currentWave, _enemyTemplates] call FUNC(spawnEnemyWave);
_currentWave = _currentWave + 1;
_nextWaveTime = serverTime + _waveCooldown;
@ -152,7 +154,7 @@ GVAR(DefendTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
true
}],
["handleSuccessOutcome", compileFinal {
@ -174,7 +176,7 @@ GVAR(DefendTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
true
}],
["runLoop", compileFinal {

View File

@ -125,7 +125,7 @@ GVAR(DefuseTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
true
}],
["handleSuccessOutcome", compileFinal {
@ -152,7 +152,7 @@ GVAR(DefuseTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
true
}],
["runLoop", compileFinal {

View File

@ -145,7 +145,7 @@ GVAR(DeliveryTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
true
}],
["handleSuccessOutcome", compileFinal {
@ -170,7 +170,7 @@ GVAR(DeliveryTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
true
}],
["runLoop", compileFinal {

View File

@ -125,7 +125,7 @@ GVAR(DestroyTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
true
}],
["handleSuccessOutcome", compileFinal {
@ -150,7 +150,7 @@ GVAR(DestroyTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
true
}],
["runLoop", compileFinal {

View File

@ -121,6 +121,9 @@ GVAR(HVTTaskBaseClass) = createHashMapFromArray [
_timeExpired = (serverTime - _startedAt) >= _timeLimit;
};
private _captureSucceeded = _capture && { _inZone >= _required } && { _killed < _maxKilled };
private _eliminateSucceeded = _eliminate && { _killed >= _required };
createHashMapFromArray [
["captives", _captives],
["killed", _killed],
@ -128,8 +131,8 @@ GVAR(HVTTaskBaseClass) = createHashMapFromArray [
["required", _required],
["maxKilled", _maxKilled],
["timeExpired", _timeExpired],
["shouldFail", (_capture && { _killed >= _maxKilled }) || { _timeExpired && { (_capture && { _captives < _required }) || { _eliminate && { _killed < _required } } } }],
["shouldSucceed", (_capture && { _inZone >= _required } && { _killed < _maxKilled }) || { _eliminate && { _killed >= _required } }]
["shouldFail", (_capture && { _killed >= _maxKilled }) || { _timeExpired && { (_capture && { !_captureSucceeded }) || { _eliminate && { !_eliminateSucceeded } } } }],
["shouldSucceed", _captureSucceeded || _eliminateSucceeded]
]
}],
["handleFailureOutcome", compileFinal {
@ -152,7 +155,7 @@ GVAR(HVTTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
true
}],
["handleSuccessOutcome", compileFinal {
@ -177,7 +180,7 @@ GVAR(HVTTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
true
}],
["runLoop", compileFinal {

View File

@ -234,6 +234,9 @@ GVAR(HostageTaskBaseClass) = createHashMapFromArray [
_timeExpired = (serverTime - _startedAt) >= _timeLimit;
};
private _hostageSucceeded = (_inZone >= _requiredRescues) && { _killed < _maxHostageLosses };
private _shootersClearedSucceeded = (_shootersAlive <= 0) && { _hostageSucceeded };
createHashMapFromArray [
["freed", _freed],
["inZone", _inZone],
@ -242,8 +245,8 @@ GVAR(HostageTaskBaseClass) = createHashMapFromArray [
["requiredRescues", _requiredRescues],
["maxHostageLosses", _maxHostageLosses],
["timeExpired", _timeExpired],
["shouldFail", (_killed >= _maxHostageLosses) || { _timeExpired && { _freed < _requiredRescues } }],
["shouldSucceed", ((_inZone >= _requiredRescues) && { _killed < _maxHostageLosses }) || { (_shootersAlive <= 0) && { _inZone >= _requiredRescues } && { _killed < _maxHostageLosses } }]
["shouldFail", (_killed >= _maxHostageLosses) || { _timeExpired && { !_hostageSucceeded } }],
["shouldSucceed", _hostageSucceeded || _shootersClearedSucceeded]
]
}],
["handleFailureOutcome", compileFinal {
@ -301,7 +304,7 @@ GVAR(HostageTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
true
}],
["handleSuccessOutcome", compileFinal {
@ -328,7 +331,7 @@ GVAR(HostageTaskBaseClass) = createHashMapFromArray [
GVAR(TaskStore) call ["clearTask", [_taskID]];
};
if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
if (_endSuccess) then { "EveryoneWon" call BFUNC(endMissionServer); };
true
}],
["runLoop", compileFinal {

View File

@ -199,7 +199,8 @@ Available task modules:
- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and
`FORGE_Module_Shooters`.
- `FORGE_Module_HVT`: sync directly to HVT units.
- `FORGE_Module_Defend`: configure the defense marker and wave settings.
- `FORGE_Module_Defend`: configure the defense marker and wave settings; sync
enemy units to use their groups as wave templates.
These modules delegate to `forge_server_task_fnc_startTask`.
@ -411,12 +412,16 @@ Setup:
6. Set `WaveCount`.
7. Set `WaveCooldown`.
8. Set `MinBlufor` to the minimum number of friendlies required in the zone.
9. Set rewards, rating, and end-state options.
9. Place one or more enemy groups or units to use as wave templates.
10. Sync any unit from each enemy group to the defend module.
11. Set rewards, rating, and end-state options.
Notes:
- No enemy groups need to be pre-placed or synced. The defend task spawns its
own enemy waves.
- Synced enemy units are treated as templates. Syncing one unit from a group
makes the whole group available as a wave composition.
- If no enemy units are synced, the defend task falls back to default CSAT
infantry waves.
- The defend task waits for the required number of BLUFOR to enter the zone
before the timer, waves, and empty-zone failure checks begin.
- `DefenseZone` must be an area marker.