From e6eceac4ec293cd925294727b742b00c4341e66c Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Thu, 14 May 2026 19:20:44 -0500 Subject: [PATCH] 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 --- arma/server/addons/task/CfgVehicles.hpp | 8 +- arma/server/addons/task/README.md | 3 +- .../addons/task/functions/fnc_attack.sqf | 4 +- .../addons/task/functions/fnc_defend.sqf | 20 ++-- .../addons/task/functions/fnc_defuse.sqf | 6 +- .../addons/task/functions/fnc_delivery.sqf | 6 +- .../addons/task/functions/fnc_destroy.sqf | 6 +- .../addons/task/functions/fnc_hostage.sqf | 22 ++--- arma/server/addons/task/functions/fnc_hvt.sqf | 23 +++-- .../addons/task/functions/fnc_makeHostage.sqf | 2 - .../functions/helpers/fnc_spawnEnemyWave.sqf | 99 ++++++++++++++----- .../task/functions/helpers/fnc_startTask.sqf | 4 +- .../functions/modules/fnc_defendModule.sqf | 46 ++++++++- .../prototypes/fnc_AttackTaskBaseClass.sqf | 4 +- .../prototypes/fnc_DefendTaskBaseClass.sqf | 8 +- .../prototypes/fnc_DefuseTaskBaseClass.sqf | 4 +- .../prototypes/fnc_DeliveryTaskBaseClass.sqf | 4 +- .../prototypes/fnc_DestroyTaskBaseClass.sqf | 4 +- .../prototypes/fnc_HVTTaskBaseClass.sqf | 11 ++- .../prototypes/fnc_HostageTaskBaseClass.sqf | 11 ++- docs/TASK_USAGE_GUIDE.md | 13 ++- 21 files changed, 211 insertions(+), 97 deletions(-) diff --git a/arma/server/addons/task/CfgVehicles.hpp b/arma/server/addons/task/CfgVehicles.hpp index 5f774a6..96001bb 100644 --- a/arma/server/addons/task/CfgVehicles.hpp +++ b/arma/server/addons/task/CfgVehicles.hpp @@ -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; diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md index c5dab82..e651fcc 100644 --- a/arma/server/addons/task/README.md +++ b/arma/server/addons/task/README.md @@ -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`. diff --git a/arma/server/addons/task/functions/fnc_attack.sqf b/arma/server/addons/task/functions/fnc_attack.sqf index a51955e..4867f02 100644 --- a/arma/server/addons/task/functions/fnc_attack.sqf +++ b/arma/server/addons/task/functions/fnc_attack.sqf @@ -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); }; }; diff --git a/arma/server/addons/task/functions/fnc_defend.sqf b/arma/server/addons/task/functions/fnc_defend.sqf index e5c4f07..ff9c1fc 100644 --- a/arma/server/addons/task/functions/fnc_defend.sqf +++ b/arma/server/addons/task/functions/fnc_defend.sqf @@ -16,11 +16,12 @@ * 8: Enemy wave count (default: 3) * 9: Time between waves in seconds (default: 300) * 10: Minimum BLUFOR units required in zone (default: 1) - * 11: Equipment rewards (default: []) - * 12: Supply rewards (default: []) - * 13: Weapon rewards (default: []) - * 14: Vehicle rewards (default: []) - * 15: Special rewards (default: []) + * 11: Enemy template groups (default: []) + * 12: Equipment rewards (default: []) + * 13: Supply rewards (default: []) + * 14: Weapon rewards (default: []) + * 15: Vehicle rewards (default: []) + * 16: Special rewards (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); }; }; diff --git a/arma/server/addons/task/functions/fnc_defuse.sqf b/arma/server/addons/task/functions/fnc_defuse.sqf index 1019bcf..fea327d 100644 --- a/arma/server/addons/task/functions/fnc_defuse.sqf +++ b/arma/server/addons/task/functions/fnc_defuse.sqf @@ -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]]; diff --git a/arma/server/addons/task/functions/fnc_delivery.sqf b/arma/server/addons/task/functions/fnc_delivery.sqf index d47cb5a..4c61fd7 100644 --- a/arma/server/addons/task/functions/fnc_delivery.sqf +++ b/arma/server/addons/task/functions/fnc_delivery.sqf @@ -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); }; }; diff --git a/arma/server/addons/task/functions/fnc_destroy.sqf b/arma/server/addons/task/functions/fnc_destroy.sqf index 3a24228..95018b3 100644 --- a/arma/server/addons/task/functions/fnc_destroy.sqf +++ b/arma/server/addons/task/functions/fnc_destroy.sqf @@ -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); }; }; diff --git a/arma/server/addons/task/functions/fnc_hostage.sqf b/arma/server/addons/task/functions/fnc_hostage.sqf index d631137..ca174fd 100644 --- a/arma/server/addons/task/functions/fnc_hostage.sqf +++ b/arma/server/addons/task/functions/fnc_hostage.sqf @@ -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); }; }; diff --git a/arma/server/addons/task/functions/fnc_hvt.sqf b/arma/server/addons/task/functions/fnc_hvt.sqf index a7548a1..b4d1cff 100644 --- a/arma/server/addons/task/functions/fnc_hvt.sqf +++ b/arma/server/addons/task/functions/fnc_hvt.sqf @@ -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); }; }; diff --git a/arma/server/addons/task/functions/fnc_makeHostage.sqf b/arma/server/addons/task/functions/fnc_makeHostage.sqf index 3a34ed1..2082fed 100644 --- a/arma/server/addons/task/functions/fnc_makeHostage.sqf +++ b/arma/server/addons/task/functions/fnc_makeHostage.sqf @@ -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]; diff --git a/arma/server/addons/task/functions/helpers/fnc_spawnEnemyWave.sqf b/arma/server/addons/task/functions/helpers/fnc_spawnEnemyWave.sqf index 1bc1bc5..8bc0229 100644 --- a/arma/server/addons/task/functions/helpers/fnc_spawnEnemyWave.sqf +++ b/arma/server/addons/task/functions/helpers/fnc_spawnEnemyWave.sqf @@ -8,6 +8,7 @@ * 0: Defense zone marker name * 1: Task ID * 2: Wave number (0-based) + * 3: Enemy template groups (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); +}; diff --git a/arma/server/addons/task/functions/helpers/fnc_startTask.sqf b/arma/server/addons/task/functions/helpers/fnc_startTask.sqf index 89c08af..cdd9825 100644 --- a/arma/server/addons/task/functions/helpers/fnc_startTask.sqf +++ b/arma/server/addons/task/functions/helpers/fnc_startTask.sqf @@ -47,6 +47,7 @@ * "waveCount" (default: 3) * "waveCooldown" (default: 300) * "minBlufor" (default: 1) + * "enemyTemplates" (default: []) * 7: Minimum org reputation required (default: 0) * 8: Requester UID (default: "") * 9: Source tag (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); diff --git a/arma/server/addons/task/functions/modules/fnc_defendModule.sqf b/arma/server/addons/task/functions/modules/fnc_defendModule.sqf index 5b12280..bdd074a 100644 --- a/arma/server/addons/task/functions/modules/fnc_defendModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_defendModule.sqf @@ -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 @@ -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], diff --git a/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf index af20e47..c2eef10 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf @@ -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", []]; diff --git a/arma/server/addons/task/functions/prototypes/fnc_DefendTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_DefendTaskBaseClass.sqf index aca6d41..8547176 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_DefendTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_DefendTaskBaseClass.sqf @@ -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 { diff --git a/arma/server/addons/task/functions/prototypes/fnc_DefuseTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_DefuseTaskBaseClass.sqf index 9917ba0..ce43a3d 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_DefuseTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_DefuseTaskBaseClass.sqf @@ -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 { diff --git a/arma/server/addons/task/functions/prototypes/fnc_DeliveryTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_DeliveryTaskBaseClass.sqf index 424df77..61f18fc 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_DeliveryTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_DeliveryTaskBaseClass.sqf @@ -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 { diff --git a/arma/server/addons/task/functions/prototypes/fnc_DestroyTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_DestroyTaskBaseClass.sqf index 62edf56..ad0ba1d 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_DestroyTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_DestroyTaskBaseClass.sqf @@ -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 { diff --git a/arma/server/addons/task/functions/prototypes/fnc_HVTTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_HVTTaskBaseClass.sqf index 9ba2d70..1c4de90 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_HVTTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_HVTTaskBaseClass.sqf @@ -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 { diff --git a/arma/server/addons/task/functions/prototypes/fnc_HostageTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_HostageTaskBaseClass.sqf index 8456fd3..89e5abd 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_HostageTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_HostageTaskBaseClass.sqf @@ -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 { diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md index ccff4f0..4c0c6a8 100644 --- a/docs/TASK_USAGE_GUIDE.md +++ b/docs/TASK_USAGE_GUIDE.md @@ -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.