Integrate mission generators into framework
This commit is contained in:
parent
1454a29de9
commit
9c2a09eed9
@ -30,6 +30,7 @@ GVAR(CADRepository) = createHashMapObject [[
|
||||
_self set ["requests", []];
|
||||
_self set ["assignments", []];
|
||||
_self set ["activity", []];
|
||||
_self set ["generatedTaskTypes", []];
|
||||
_self set ["session", createHashMap];
|
||||
_self set ["mode", "operations"];
|
||||
_self set ["dispatchView", "board"];
|
||||
@ -41,6 +42,7 @@ GVAR(CADRepository) = createHashMapObject [[
|
||||
["requests", +(_self getOrDefault ["requests", []])],
|
||||
["assignments", +(_self getOrDefault ["assignments", []])],
|
||||
["activity", +(_self getOrDefault ["activity", []])],
|
||||
["generatedTaskTypes", +(_self getOrDefault ["generatedTaskTypes", []])],
|
||||
["session", +(_self getOrDefault ["session", createHashMap])],
|
||||
["mode", _self getOrDefault ["mode", "operations"]],
|
||||
["dispatchView", _self getOrDefault ["dispatchView", "board"]]
|
||||
@ -72,6 +74,7 @@ GVAR(CADRepository) = createHashMapObject [[
|
||||
_self set ["requests", +(_payload getOrDefault ["requests", []])];
|
||||
_self set ["assignments", +(_payload getOrDefault ["assignments", []])];
|
||||
_self set ["activity", +(_payload getOrDefault ["activity", []])];
|
||||
_self set ["generatedTaskTypes", +(_payload getOrDefault ["generatedTaskTypes", []])];
|
||||
_self set ["session", +(_payload getOrDefault ["session", createHashMap])];
|
||||
true
|
||||
}],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,16 @@
|
||||
const dispatcherFormatters = window.cadDispatcherFormatters || {};
|
||||
const dispatcherModals = window.cadDispatcherModals || {};
|
||||
const dispatcherRender = window.cadDispatcherRender || {};
|
||||
const defaultTaskTypes = [
|
||||
{ value: "attack", label: "Attack" },
|
||||
{ value: "defend", label: "Defend" },
|
||||
{ value: "delivery", label: "Delivery" },
|
||||
{ value: "destroy", label: "Destroy" },
|
||||
{ value: "defuse", label: "Defuse" },
|
||||
{ value: "hostage", label: "Hostage" },
|
||||
{ value: "hvtkill", label: "Kill HVT" },
|
||||
{ value: "hvtcapture", label: "Capture HVT" },
|
||||
];
|
||||
|
||||
window.cadDispatcher = {
|
||||
contracts: [],
|
||||
@ -11,16 +21,7 @@ window.cadDispatcher = {
|
||||
editingGroupId: "",
|
||||
viewingRequestId: "",
|
||||
convertingRequestId: "",
|
||||
taskTypes: [
|
||||
{ value: "attack", label: "Attack" },
|
||||
{ value: "defend", label: "Defend" },
|
||||
{ value: "delivery", label: "Delivery" },
|
||||
{ value: "destroy", label: "Destroy" },
|
||||
{ value: "defuse", label: "Defuse" },
|
||||
{ value: "hostage", label: "Hostage" },
|
||||
{ value: "hvtkill", label: "Kill HVT" },
|
||||
{ value: "hvtcapture", label: "Capture HVT" },
|
||||
],
|
||||
taskTypes: defaultTaskTypes.slice(),
|
||||
statuses: [
|
||||
"available",
|
||||
"en_route",
|
||||
@ -133,6 +134,14 @@ window.cadDispatcher = {
|
||||
this.requests = Array.isArray(payload.requests) ? payload.requests : [];
|
||||
this.groups = Array.isArray(payload.groups) ? payload.groups : [];
|
||||
this.activity = Array.isArray(payload.activity) ? payload.activity : [];
|
||||
if (Array.isArray(payload.generatedTaskTypes)) {
|
||||
this.taskTypes = payload.generatedTaskTypes
|
||||
.map((entry) => ({
|
||||
value: String(entry?.value || "").trim(),
|
||||
label: String(entry?.label || entry?.value || "").trim(),
|
||||
}))
|
||||
.filter((entry) => entry.value);
|
||||
}
|
||||
this.session =
|
||||
payload.session && typeof payload.session === "object"
|
||||
? payload.session
|
||||
@ -223,6 +232,14 @@ window.cadDispatcher = {
|
||||
this.closeOrderModal();
|
||||
},
|
||||
requestGeneratedTask() {
|
||||
if (!this.taskTypes.length) {
|
||||
this.setStatus(
|
||||
"Generated task requests are disabled by server settings.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const taskType = document.getElementById(
|
||||
"dispatcherTaskTypeSelect",
|
||||
).value;
|
||||
|
||||
@ -18,6 +18,21 @@ window.cadDispatcherModals = {
|
||||
return;
|
||||
}
|
||||
|
||||
const saveButton = document.getElementById(
|
||||
"dispatcherTaskModalSaveBtn",
|
||||
);
|
||||
const hasTaskTypes = this.taskTypes.length > 0;
|
||||
taskTypeSelect.disabled = !hasTaskTypes;
|
||||
if (saveButton) {
|
||||
saveButton.disabled = !hasTaskTypes;
|
||||
}
|
||||
|
||||
if (!hasTaskTypes) {
|
||||
taskTypeSelect.innerHTML =
|
||||
'<option value="">No generated tasks available</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
taskTypeSelect.innerHTML = this.buildTaskTypeOptions(
|
||||
taskTypeSelect.value || this.taskTypes[0]?.value || "",
|
||||
);
|
||||
|
||||
@ -91,20 +91,23 @@ call FUNC(registerEventListeners);
|
||||
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
if (isNil "forge_pmc_fnc_requestMissionTask") exitWith {
|
||||
_result set ["message", "This mission does not expose dispatcher-generated tasks."];
|
||||
if !(isNil QEFUNC(task,requestMissionTask)) then {
|
||||
_result = [_taskType, _metadata, _uid] call EFUNC(task,requestMissionTask);
|
||||
} else {
|
||||
if (isNil "forge_pmc_fnc_requestMissionTask") exitWith {
|
||||
_result set ["message", "This mission does not expose dispatcher-generated tasks."];
|
||||
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
_result = [_taskType, _metadata, _uid] call forge_pmc_fnc_requestMissionTask;
|
||||
};
|
||||
|
||||
if !(_result getOrDefault ["success", false]) exitWith {
|
||||
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
// Temporary mission-owned integration point. This keeps simulator-specific
|
||||
// generator logic in the mission until CAD/task grows a framework-level
|
||||
// on-demand generation interface.
|
||||
_result = [_taskType, _metadata, _uid] call forge_pmc_fnc_requestMissionTask;
|
||||
[CRPC(cad,responseCadRequest), [_result], _player] call CFUNC(targetEvent);
|
||||
|
||||
if (_result getOrDefault ["success", false]) then {
|
||||
[CRPC(cad,invalidateCadState), []] call CFUNC(globalEvent);
|
||||
};
|
||||
[CRPC(cad,invalidateCadState), []] call CFUNC(globalEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestSubmitCadSupportRequest), {
|
||||
|
||||
@ -299,6 +299,16 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _permissionService = _self get "PermissionService";
|
||||
private _groupRepository = _self get "GroupRepository";
|
||||
private _generatedTaskTypes = [];
|
||||
if (missionNamespace getVariable [QEGVAR(task,enableGenerator), false]) then {
|
||||
if (isNil QEGVAR(task,MissionManager) && { !(isNil QEFUNC(task,missionManager)) }) then {
|
||||
call EFUNC(task,missionManager);
|
||||
};
|
||||
|
||||
if !(isNil QEGVAR(task,MissionManager)) then {
|
||||
_generatedTaskTypes = EGVAR(task,MissionManager) call ["getGeneratedTaskTypes", []];
|
||||
};
|
||||
};
|
||||
|
||||
private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]];
|
||||
private _session = createHashMapFromArray [
|
||||
@ -311,6 +321,7 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _seed = createHashMapFromArray [
|
||||
["groups", _groupRepository call ["buildGroups", []]],
|
||||
["activeTasks", EGVAR(task,TaskStore) call ["getActiveTaskCatalog", []]],
|
||||
["generatedTaskTypes", _generatedTaskTypes],
|
||||
["session", _session]
|
||||
];
|
||||
private _emptyPayload = createHashMapFromArray [
|
||||
@ -319,6 +330,7 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
["requests", []],
|
||||
["assignments", []],
|
||||
["activity", []],
|
||||
["generatedTaskTypes", _generatedTaskTypes],
|
||||
["session", _session]
|
||||
];
|
||||
private _persistenceService = _self getOrDefault ["PersistenceService", createHashMap];
|
||||
@ -330,7 +342,9 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _hydrateResult = _persistenceService call ["buildHydratePayload", [_seed]];
|
||||
if (_hydrateResult getOrDefault ["success", false]) exitWith {
|
||||
_hydrateResult getOrDefault ["data", createHashMap]
|
||||
private _data = _hydrateResult getOrDefault ["data", createHashMap];
|
||||
_data set ["generatedTaskTypes", _generatedTaskTypes];
|
||||
_data
|
||||
};
|
||||
|
||||
["WARNING", "CAD hydrate failed in the extension; returning seed-only payload."] call EFUNC(common,log);
|
||||
|
||||
@ -1,167 +1,68 @@
|
||||
// TODO: Move to mission template and provide documentation
|
||||
/*
|
||||
* PMC simulator dynamic mission configuration.
|
||||
*
|
||||
* This file is read by the mission setup UI, the mission manager, and the
|
||||
* mission generators under functions\missionGenerators.
|
||||
*
|
||||
* Startup UI behavior:
|
||||
* - Arma mission params/defaults provide the startup setup UI defaults.
|
||||
* - If the setup UI is cancelled, those same params/defaults are applied.
|
||||
* - If the setup UI is submitted, UI values override compatible ranges.
|
||||
*
|
||||
* Generator behavior:
|
||||
* - maxConcurrentMissions and missionInterval are copied into
|
||||
* forge_pmc_missionSettings by forge_pmc_fnc_setupMenu_applySettings.
|
||||
* - Reward, reputation, penalty, and timeLimit ranges are read through
|
||||
* forge_pmc_fnc_getMissionSettingRange so UI overrides and config fallbacks
|
||||
* use the same path.
|
||||
*/
|
||||
class CfgMissions {
|
||||
// Global settings
|
||||
// Maximum number of generated missions allowed to be active at once.
|
||||
maxConcurrentMissions = 3;
|
||||
missionInterval = 300; // 5 minutes between mission generation
|
||||
|
||||
// Mission type weights
|
||||
|
||||
// Seconds between mission generation attempts.
|
||||
missionInterval = 300;
|
||||
|
||||
// Seconds before a generated mission location can be reused.
|
||||
locationReuseCooldown = 900;
|
||||
|
||||
// Enemy faction selection is ultimately exported to ENEMY_FACTION_STR and
|
||||
// ENEMY_SIDE for server-side generators.
|
||||
class EnemyFactionConfig {
|
||||
// Mission param key used by fallback/default setup application.
|
||||
enemyFactionParam = "enemyFaction";
|
||||
};
|
||||
|
||||
// Relative generation weights. The values do not need to add to 1; the
|
||||
// mission manager treats them as weighted proportions.
|
||||
class MissionWeights {
|
||||
attack = 0.2;
|
||||
defend = 0.2;
|
||||
hostage = 0.2;
|
||||
hvt = 0.15;
|
||||
hvtkill = 0.15;
|
||||
hvtcapture = 0.15;
|
||||
defuse = 0.15;
|
||||
delivery = 0.1;
|
||||
destroy = 0.2;
|
||||
};
|
||||
|
||||
// Mission locations
|
||||
class Locations {
|
||||
class CityOne {
|
||||
position[] = {1000, 1000, 0};
|
||||
type = "city";
|
||||
radius = 300;
|
||||
suitable[] = {"attack", "defend", "hostage"};
|
||||
};
|
||||
class MilitaryBase {
|
||||
position[] = {2000, 2000, 0};
|
||||
type = "military";
|
||||
radius = 500;
|
||||
suitable[] = {"hvt", "defend", "attack"};
|
||||
};
|
||||
class Industrial {
|
||||
position[] = {3000, 3000, 0};
|
||||
type = "industrial";
|
||||
radius = 200;
|
||||
suitable[] = {"delivery", "defuse"};
|
||||
};
|
||||
};
|
||||
|
||||
// AI Groups configuration
|
||||
class AIGroups {
|
||||
class Infantry {
|
||||
side = "EAST";
|
||||
class Units {
|
||||
class Unit0 {
|
||||
vehicle = "O_Soldier_TL_F";
|
||||
rank = "SERGEANT";
|
||||
position[] = {0, 0, 0};
|
||||
};
|
||||
class Unit1 {
|
||||
vehicle = "O_Soldier_AR_F";
|
||||
rank = "CORPORAL";
|
||||
position[] = {5, -5, 0};
|
||||
};
|
||||
class Unit2 {
|
||||
vehicle = "O_Soldier_LAT_F";
|
||||
rank = "PRIVATE";
|
||||
position[] = {-5, -5, 0};
|
||||
};
|
||||
};
|
||||
suitable[] = {"attack", "defend", "hostage"};
|
||||
};
|
||||
class Assault {
|
||||
side = "EAST";
|
||||
class Units {
|
||||
class Unit0 {
|
||||
vehicle = "O_Soldier_SL_F";
|
||||
rank = "SERGEANT";
|
||||
position[] = {0, 0, 0};
|
||||
};
|
||||
class Unit1 {
|
||||
vehicle = "O_Soldier_GL_F";
|
||||
rank = "CORPORAL";
|
||||
position[] = {4, -3, 0};
|
||||
};
|
||||
class Unit2 {
|
||||
vehicle = "O_Soldier_AR_F";
|
||||
rank = "CORPORAL";
|
||||
position[] = {-4, -3, 0};
|
||||
};
|
||||
class Unit3 {
|
||||
vehicle = "O_medic_F";
|
||||
rank = "PRIVATE";
|
||||
position[] = {7, -6, 0};
|
||||
};
|
||||
};
|
||||
suitable[] = {"attack", "defend"};
|
||||
};
|
||||
class MotorizedPatrol {
|
||||
side = "EAST";
|
||||
class Units {
|
||||
class Unit0 {
|
||||
vehicle = "O_Soldier_TL_F";
|
||||
rank = "SERGEANT";
|
||||
position[] = {0, 0, 0};
|
||||
};
|
||||
class Unit1 {
|
||||
vehicle = "O_Soldier_LAT_F";
|
||||
rank = "CORPORAL";
|
||||
position[] = {5, -4, 0};
|
||||
};
|
||||
class Unit2 {
|
||||
vehicle = "O_Soldier_F";
|
||||
rank = "PRIVATE";
|
||||
position[] = {-5, -4, 0};
|
||||
};
|
||||
class Unit3 {
|
||||
vehicle = "O_Soldier_A_F";
|
||||
rank = "PRIVATE";
|
||||
position[] = {8, -7, 0};
|
||||
};
|
||||
};
|
||||
suitable[] = {"attack", "defend"};
|
||||
};
|
||||
class SpecOps {
|
||||
side = "EAST";
|
||||
class Units {
|
||||
class Unit0 {
|
||||
vehicle = "O_recon_TL_F";
|
||||
rank = "SERGEANT";
|
||||
position[] = {0, 0, 0};
|
||||
};
|
||||
class Unit1 {
|
||||
vehicle = "O_recon_M_F";
|
||||
rank = "CORPORAL";
|
||||
position[] = {5, -5, 0};
|
||||
};
|
||||
};
|
||||
suitable[] = {"hvt", "hostage"};
|
||||
};
|
||||
class ReconRaid {
|
||||
side = "EAST";
|
||||
class Units {
|
||||
class Unit0 {
|
||||
vehicle = "O_recon_TL_F";
|
||||
rank = "SERGEANT";
|
||||
position[] = {0, 0, 0};
|
||||
};
|
||||
class Unit1 {
|
||||
vehicle = "O_recon_M_F";
|
||||
rank = "CORPORAL";
|
||||
position[] = {4, -4, 0};
|
||||
};
|
||||
class Unit2 {
|
||||
vehicle = "O_recon_LAT_F";
|
||||
rank = "CORPORAL";
|
||||
position[] = {-4, -4, 0};
|
||||
};
|
||||
class Unit3 {
|
||||
vehicle = "O_recon_medic_F";
|
||||
rank = "PRIVATE";
|
||||
position[] = {7, -7, 0};
|
||||
};
|
||||
};
|
||||
suitable[] = {"attack", "hvt", "hostage"};
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Continue to refine mission types and their specific settings
|
||||
// Mission type specific settings
|
||||
/*
|
||||
* Mission type settings.
|
||||
*
|
||||
* Common fields:
|
||||
* - Rewards.money[]: min/max funds reward.
|
||||
* - Rewards.reputation[]: min/max reputation reward.
|
||||
* - Rewards.<category>[]: item reward rolls as {classname, chance}.
|
||||
* - penalty[]: numeric min/max reputation penalty on failure. UI settings
|
||||
* may express these as min/max reputation hits, then the helper sorts the
|
||||
* numeric roll range before generators use it.
|
||||
* - timeLimit[]: min/max task time limit in seconds.
|
||||
*/
|
||||
class MissionTypes {
|
||||
// Search-and-destroy infantry engagement.
|
||||
class Attack {
|
||||
minUnits = 4;
|
||||
maxUnits = 8;
|
||||
patrolRadius = 200;
|
||||
class Rewards {
|
||||
money[] = {25000, 60000};
|
||||
reputation[] = {6, 14};
|
||||
@ -172,13 +73,16 @@ class CfgMissions {
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-8, -3};
|
||||
timeLimit[] = {900, 1800}; // 15-30 minutes
|
||||
timeLimit[] = {900, 1800};
|
||||
};
|
||||
|
||||
|
||||
// Hold a generated position through multiple enemy waves.
|
||||
class Defend {
|
||||
minWaves = 3;
|
||||
maxWaves = 8;
|
||||
// Min/max units spawned per wave before active-player scaling.
|
||||
unitsPerWave[] = {4, 8};
|
||||
// Seconds between wave spawns.
|
||||
waveCooldown = 300;
|
||||
class Rewards {
|
||||
money[] = {40000, 90000};
|
||||
@ -190,13 +94,15 @@ class CfgMissions {
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-12, -4};
|
||||
timeLimit[] = {1800, 3600}; // 30-60 minutes
|
||||
timeLimit[] = {300, 1800};
|
||||
};
|
||||
|
||||
|
||||
// Rescue a hostage from a generated hostile site.
|
||||
class Hostage {
|
||||
// Candidate hostage classnames by broad source category.
|
||||
class Hostages {
|
||||
civilian[] = {"C_man_1", "C_man_polo_1_F"};
|
||||
military[] = {"B_Pilot_F", "B_officer_F"};
|
||||
civilian[] = {"C_journalist_F", "C_Journalist_01_War_F", "C_Man_Paramedic_01_F", "C_scientist_F", "C_IDAP_Pilot_RF", "C_IDAP_Man_Paramedic_01_F", "C_IDAP_Pilot_01_F", "C_IDAP_Man_AidWorker_01_F", "C_IDAP_Man_AidWorker_05_F", "C_pilot_story_RF", "C_pilot2_story_RF", "C_Orestes", "C_Nikos", "C_Journalist_lxWS"};
|
||||
military[] = {"B_helicrew_F", "B_Helipilot_F", "B_officer_F", "B_Fighter_Pilot_F", "B_Captain_Jay_F", "B_CTRG_soldier_M_medic_F", "B_Story_Pilot_F", "B_CTRG_soldier_GL_LAT_F", "B_Captain_Pettka_F", "B_Survivor_F", "B_Pilot_F"};
|
||||
};
|
||||
class Rewards {
|
||||
money[] = {60000, 140000};
|
||||
@ -208,14 +114,17 @@ class CfgMissions {
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-16, -6};
|
||||
timeLimit[] = {600, 900}; // 10-15 minutes
|
||||
timeLimit[] = {600, 900};
|
||||
};
|
||||
|
||||
class HVT {
|
||||
// Eliminate a high-value target with escort security.
|
||||
class HVTKill {
|
||||
// Candidate target classnames by role.
|
||||
class Targets {
|
||||
officer[] = {"O_officer_F"};
|
||||
sniper[] = {"O_sniper_F"};
|
||||
};
|
||||
// Number of escort units to attempt around the target.
|
||||
escorts = 4;
|
||||
class Rewards {
|
||||
money[] = {50000, 120000};
|
||||
@ -227,15 +136,40 @@ class CfgMissions {
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-14, -5};
|
||||
timeLimit[] = {900, 1800}; // 15-30 minutes
|
||||
timeLimit[] = {900, 1800};
|
||||
};
|
||||
|
||||
class Defuse {
|
||||
class Devices {
|
||||
small[] = {"DemoCharge_Remote_Mag"};
|
||||
large[] = {"SatchelCharge_Remote_Mag"};
|
||||
// Capture and extract a high-value target.
|
||||
class HVTCapture {
|
||||
// Candidate capturable target classnames.
|
||||
class Targets {
|
||||
civilian[] = {"C_journalist_F", "C_Journalist_01_War_F", "C_Man_Paramedic_01_F", "C_scientist_F", "C_IDAP_Pilot_RF", "C_IDAP_Man_Paramedic_01_F", "C_IDAP_Pilot_01_F", "C_IDAP_Man_AidWorker_01_F", "C_IDAP_Man_AidWorker_05_F", "C_pilot_story_RF", "C_pilot2_story_RF", "C_Orestes", "C_Nikos", "C_Journalist_lxWS"};
|
||||
};
|
||||
maxDevices = 3;
|
||||
// Number of escort units to attempt around the target.
|
||||
escorts = 4;
|
||||
class Rewards {
|
||||
money[] = {50000, 120000};
|
||||
reputation[] = {10, 22};
|
||||
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
|
||||
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
|
||||
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
|
||||
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-14, -5};
|
||||
timeLimit[] = {900, 1800};
|
||||
};
|
||||
|
||||
// Defuse explosive devices and protect nearby critical objects.
|
||||
class Defuse {
|
||||
// Device and protected-object candidate classnames.
|
||||
class Devices {
|
||||
small[] = {"DemoCharge_F", "IEDLandSmall_F", "IEDUrbanSmall_F", "ACE_IEDLandSmall_Range", "ACE_IEDUrbanSmall_Range"};
|
||||
large[] = {"SatchelCharge_F", "IEDLandBig_F", "IEDUrbanBig_F", "ACE_IEDLandBig_Range", "ACE_IEDUrbanBig_Range"};
|
||||
protected[] = {"CargoNet_01_barrels_F", "CargoNet_01_box_F", "B_CargoNet_01_ammo_F", "C_IDAP_CargoNet_01_supplies_F", "Box_NATO_AmmoVeh_F", "B_supplyCrate_F"};
|
||||
};
|
||||
// Maximum explosive devices to place for one generated task.
|
||||
maxDevices = 1;
|
||||
class Rewards {
|
||||
money[] = {20000, 50000};
|
||||
reputation[] = {5, 12};
|
||||
@ -246,13 +180,15 @@ class CfgMissions {
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-9, -3};
|
||||
timeLimit[] = {600, 900}; // 10-15 minutes
|
||||
timeLimit[] = {600, 900};
|
||||
};
|
||||
|
||||
// Deliver cargo or vehicles between generated locations.
|
||||
class Delivery {
|
||||
// Candidate delivery objects grouped by cargo type.
|
||||
class Cargo {
|
||||
supplies[] = {"Land_CargoBox_V1_F"};
|
||||
vehicles[] = {"B_MRAP_01_F", "B_Truck_01_transport_F"};
|
||||
supplies[] = {"CargoNet_01_barrels_F", "CargoNet_01_box_F", "B_CargoNet_01_ammo_F", "C_IDAP_CargoNet_01_supplies_F", "Box_NATO_AmmoVeh_F", "B_supplyCrate_F"};
|
||||
vehicles[] = {};
|
||||
};
|
||||
class Rewards {
|
||||
money[] = {10000, 30000};
|
||||
@ -264,7 +200,26 @@ class CfgMissions {
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-6, -2};
|
||||
timeLimit[] = {900, 1800}; // 15-30 minutes
|
||||
timeLimit[] = {0, 0};
|
||||
};
|
||||
|
||||
// Destroy generated infrastructure targets.
|
||||
class Destroy {
|
||||
// Candidate destructible target classnames.
|
||||
class Bomb {
|
||||
building[] = {"Land_Radar_F", "Land_Radar_Small_F", "Land_MobileRadar_01_radar_F", "Land_MobileRadar_01_generator_F", "Land_Communication_F", "Land_spp_Tower_F", "Land_TTowerSmall_1_F", "Land_TTowerSmall_2_F", "Land_TTowerBig_1_F", "Land_TTowerBig_2_F"};
|
||||
};
|
||||
class Rewards {
|
||||
money[] = {10000, 30000};
|
||||
reputation[] = {3, 8};
|
||||
equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
|
||||
supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
|
||||
weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
|
||||
vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
|
||||
special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
|
||||
};
|
||||
penalty[] = {-6, -2};
|
||||
timeLimit[] = {900, 1800};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@ -95,7 +95,8 @@ Mission designers can create tasks in four ways:
|
||||
intentionally fall back to the `default` org. This path expects the BIS task
|
||||
to already exist if map-task visibility is required.
|
||||
|
||||
The dynamic mission manager can also generate attack tasks from config. That is
|
||||
The dynamic mission manager can also generate attack, defend, defuse, delivery,
|
||||
destroy, hostage, HVT kill, and HVT capture tasks from config. That is
|
||||
system-generated content rather than a hand-authored task creation path.
|
||||
|
||||
### CAD Compatibility
|
||||
@ -110,7 +111,7 @@ CAD-compatible creation paths:
|
||||
- Eden modules: compatible because they delegate to `fnc_startTask.sqf`
|
||||
- `fnc_startTask.sqf`: compatible because it registers the catalog entry,
|
||||
creates the BIS task, and dispatches through `fnc_handler.sqf`
|
||||
- dynamic mission manager attack tasks: compatible because the mission manager
|
||||
- dynamic mission manager tasks: compatible because the mission manager
|
||||
uses `fnc_startTask.sqf`
|
||||
|
||||
Limited or incompatible paths:
|
||||
@ -244,7 +245,8 @@ Task module emits the following events to the event bus:
|
||||
- `task.notification.requested` - participant notifications pending dispatch
|
||||
|
||||
## Notes
|
||||
- the dynamic mission manager in `fnc_missionManager.sqf` is initialized during task post-init; mission generation only runs when the `forge_task_enableGenerator` CBA setting is enabled
|
||||
- the dynamic mission manager in `fnc_missionManager.sqf` is initialized during task post-init; timer-based mission generation only runs when the `forge_task_enableGenerator` CBA setting is enabled
|
||||
- CAD can request a specific generated mission type through `fnc_requestMissionTask.sqf`
|
||||
- it starts server-owned tasks through `fnc_handler.sqf` and binds them to the `default` org
|
||||
- task lifecycle for the mission manager is tracked through `TaskStore` status entries
|
||||
- task backend state is intentionally transient and resets with the active server/mission lifecycle
|
||||
|
||||
@ -14,15 +14,26 @@ PREP(makeObject);
|
||||
PREP(makeShooter);
|
||||
PREP(makeTarget);
|
||||
PREP(missionManager);
|
||||
PREP(requestMissionTask);
|
||||
PREP(initTaskStore);
|
||||
|
||||
PREP_SUBDIR(generators,attackMissionGenerator);
|
||||
PREP_SUBDIR(generators,captureHvtMissionGenerator);
|
||||
PREP_SUBDIR(generators,defendMissionGenerator);
|
||||
PREP_SUBDIR(generators,defuseMissionGenerator);
|
||||
PREP_SUBDIR(generators,deliveryMissionGenerator);
|
||||
PREP_SUBDIR(generators,destroyMissionGenerator);
|
||||
PREP_SUBDIR(generators,hostageMissionGenerator);
|
||||
PREP_SUBDIR(generators,hvtMissionGenerator);
|
||||
|
||||
PREP_SUBDIR(helpers,getEnemyFactionUnitPool);
|
||||
PREP_SUBDIR(helpers,getMissionSettingRange);
|
||||
PREP_SUBDIR(helpers,handleTaskRewards);
|
||||
PREP_SUBDIR(helpers,parseTaskChainAttributes);
|
||||
PREP_SUBDIR(helpers,parseRewards);
|
||||
PREP_SUBDIR(helpers,spawnEnemyWave);
|
||||
PREP_SUBDIR(helpers,startTask);
|
||||
PREP_SUBDIR(helpers,updateEnemyCountFromActivePlayers);
|
||||
|
||||
PREP_SUBDIR(modules,attackModule);
|
||||
PREP_SUBDIR(modules,cargoModule);
|
||||
|
||||
@ -19,6 +19,13 @@
|
||||
if !(isServer) exitWith { false };
|
||||
if !(isNil QGVAR(MissionManagerPFH)) exitWith { false };
|
||||
if (isNil QGVAR(AttackMissionGeneratorBaseClass)) then { call FUNC(attackMissionGenerator); };
|
||||
if (isNil QGVAR(DefendMissionGeneratorBaseClass)) then { call FUNC(defendMissionGenerator); };
|
||||
if (isNil QGVAR(DefuseMissionGeneratorBaseClass)) then { call FUNC(defuseMissionGenerator); };
|
||||
if (isNil QGVAR(DeliveryMissionGeneratorBaseClass)) then { call FUNC(deliveryMissionGenerator); };
|
||||
if (isNil QGVAR(DestroyMissionGeneratorBaseClass)) then { call FUNC(destroyMissionGenerator); };
|
||||
if (isNil QGVAR(HostageMissionGeneratorBaseClass)) then { call FUNC(hostageMissionGenerator); };
|
||||
if (isNil QGVAR(KillHvtMissionGeneratorBaseClass)) then { call FUNC(hvtMissionGenerator); };
|
||||
if (isNil QGVAR(CaptureHvtMissionGeneratorBaseClass)) then { call FUNC(captureHvtMissionGenerator); };
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
|
||||
@ -27,11 +34,55 @@ GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
|
||||
_self set ["lastMissionGenerationAt", -1e10];
|
||||
_self set ["recentLocationRegistry", []];
|
||||
_self set ["activeMissionRegistry", createHashMap];
|
||||
_self set ["generators", [createHashMapObject [GVAR(AttackMissionGeneratorBaseClass)]]];
|
||||
_self set ["generators", [
|
||||
["attack", createHashMapObject [GVAR(AttackMissionGeneratorBaseClass)]],
|
||||
["defend", createHashMapObject [GVAR(DefendMissionGeneratorBaseClass)]],
|
||||
["defuse", createHashMapObject [GVAR(DefuseMissionGeneratorBaseClass)]],
|
||||
["delivery", createHashMapObject [GVAR(DeliveryMissionGeneratorBaseClass)]],
|
||||
["destroy", createHashMapObject [GVAR(DestroyMissionGeneratorBaseClass)]],
|
||||
["hostage", createHashMapObject [GVAR(HostageMissionGeneratorBaseClass)]],
|
||||
["hvtkill", createHashMapObject [GVAR(KillHvtMissionGeneratorBaseClass)]],
|
||||
["hvtcapture", createHashMapObject [GVAR(CaptureHvtMissionGeneratorBaseClass)]]
|
||||
]];
|
||||
}],
|
||||
["getGenerators", compileFinal {
|
||||
(_self getOrDefault ["generators", []]) apply { _x param [1, createHashMap, [createHashMap]] }
|
||||
}],
|
||||
["getGeneratorEntries", compileFinal {
|
||||
_self getOrDefault ["generators", []]
|
||||
}],
|
||||
["getGeneratorByType", compileFinal {
|
||||
params [["_generatorType", "", [""]]];
|
||||
|
||||
private _result = createHashMap;
|
||||
{
|
||||
if ((_x param [0, "", [""]]) isEqualTo _generatorType) exitWith {
|
||||
_result = _x param [1, createHashMap, [createHashMap]];
|
||||
};
|
||||
} forEach (_self call ["getGeneratorEntries", []]);
|
||||
|
||||
_result
|
||||
}],
|
||||
["getGeneratedTaskTypes", compileFinal {
|
||||
private _labels = createHashMapFromArray [
|
||||
["attack", "Attack"],
|
||||
["defend", "Defend"],
|
||||
["defuse", "Defuse"],
|
||||
["delivery", "Delivery"],
|
||||
["destroy", "Destroy"],
|
||||
["hostage", "Hostage"],
|
||||
["hvtkill", "Kill HVT"],
|
||||
["hvtcapture", "Capture HVT"]
|
||||
];
|
||||
|
||||
(_self call ["getGeneratorEntries", []]) apply {
|
||||
private _generatorType = _x param [0, "", [""]];
|
||||
createHashMapFromArray [
|
||||
["value", _generatorType],
|
||||
["label", _labels getOrDefault [_generatorType, _generatorType]]
|
||||
]
|
||||
}
|
||||
}],
|
||||
["getActiveMissionIds", compileFinal {
|
||||
private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
keys _activeMissionRegistry
|
||||
@ -119,15 +170,44 @@ GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
|
||||
""
|
||||
};
|
||||
|
||||
private _startedTaskID = "";
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
private _weightsConfig = _missionConfig >> "MissionWeights";
|
||||
private _weighted = [];
|
||||
private _totalWeight = 0;
|
||||
{
|
||||
private _taskID = _x call ["startMission", [_self]];
|
||||
if (_taskID isNotEqualTo "") exitWith {
|
||||
_startedTaskID = _taskID;
|
||||
};
|
||||
} forEach (_self call ["getGenerators", []]);
|
||||
private _generatorType = _x param [0, "", [""]];
|
||||
private _generator = _x param [1, createHashMap, [createHashMap]];
|
||||
if (_generatorType isEqualTo "" || { _generator isEqualTo createHashMap }) then { continue; };
|
||||
|
||||
_startedTaskID
|
||||
private _weight = getNumber (_weightsConfig >> _generatorType);
|
||||
if (_weight <= 0) then { _weight = 1; };
|
||||
|
||||
_totalWeight = _totalWeight + _weight;
|
||||
_weighted pushBack [_generatorType, _generator, _totalWeight];
|
||||
} forEach (_self call ["getGeneratorEntries", []]);
|
||||
|
||||
if (_weighted isEqualTo [] || { _totalWeight <= 0 }) exitWith { "" };
|
||||
|
||||
private _roll = random _totalWeight;
|
||||
private _selected = _weighted select 0;
|
||||
{
|
||||
if (_roll <= (_x param [2, 0, [0]])) exitWith {
|
||||
_selected = _x;
|
||||
};
|
||||
} forEach _weighted;
|
||||
|
||||
private _generatorType = _selected param [0, "", [""]];
|
||||
private _generator = _selected param [1, createHashMap, [createHashMap]];
|
||||
private _taskID = _generator call ["startMission", [_self]];
|
||||
if (_taskID isEqualTo "") exitWith {
|
||||
["WARNING", format ["Mission manager failed to start '%1' generated mission.", _generatorType]] call EFUNC(common,log);
|
||||
""
|
||||
};
|
||||
|
||||
_taskID
|
||||
}]
|
||||
];
|
||||
|
||||
@ -154,6 +234,8 @@ if (isNil QGVAR(MissionManagerTaskEventTokens)) then {
|
||||
|
||||
if (GVAR(enableGenerator)) then {
|
||||
GVAR(MissionManagerPFH) = [{
|
||||
if !(GVAR(enableGenerator)) exitWith {};
|
||||
|
||||
GVAR(MissionManager) call ["cleanupCompletedMissions", []];
|
||||
|
||||
private _now = diag_tickTime;
|
||||
|
||||
120
arma/server/addons/task/functions/fnc_requestMissionTask.sqf
Normal file
120
arma/server/addons/task/functions/fnc_requestMissionTask.sqf
Normal file
@ -0,0 +1,120 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Framework-owned on-demand dynamic mission request entry point for CAD and
|
||||
* other server-side dispatchers.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Generator type <STRING>
|
||||
* 1: Request metadata <HASHMAP> (Default: createHashMap)
|
||||
* 2: Requesting player UID <STRING> (Default: "")
|
||||
*
|
||||
* Return Value:
|
||||
* Request result with success, message, taskID, and taskType keys <HASHMAP>
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
if !(isServer) exitWith {
|
||||
createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Generated task requests must run on the server."]
|
||||
]
|
||||
};
|
||||
|
||||
params [
|
||||
["_requestedType", "", [""]],
|
||||
["_metadata", createHashMap, [createHashMap]],
|
||||
["_requesterUid", "", [""]]
|
||||
];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Generated task request failed."],
|
||||
["taskID", ""],
|
||||
["taskType", _requestedType]
|
||||
];
|
||||
|
||||
if !(GVAR(enableGenerator)) exitWith {
|
||||
_result set ["message", "Generated task requests are disabled by server settings."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _typeAliases = createHashMapFromArray [
|
||||
["attack", "attack"],
|
||||
["defend", "defend"],
|
||||
["defense", "defend"],
|
||||
["delivery", "delivery"],
|
||||
["deliver", "delivery"],
|
||||
["destroy", "destroy"],
|
||||
["defuse", "defuse"],
|
||||
["hostage", "hostage"],
|
||||
["hvt", "hvtkill"],
|
||||
["hvtkill", "hvtkill"],
|
||||
["killhvt", "hvtkill"],
|
||||
["kill_hvt", "hvtkill"],
|
||||
["hvtcapture", "hvtcapture"],
|
||||
["capturehvt", "hvtcapture"],
|
||||
["capture_hvt", "hvtcapture"]
|
||||
];
|
||||
|
||||
private _generatorType = _typeAliases getOrDefault [toLowerANSI _requestedType, ""];
|
||||
if (_generatorType isEqualTo "") exitWith {
|
||||
_result set ["message", format ["Unknown generated task type: %1", _requestedType]];
|
||||
_result
|
||||
};
|
||||
_result set ["taskType", _generatorType];
|
||||
|
||||
if (isNil QGVAR(TaskStore)) exitWith {
|
||||
_result set ["message", "Task store is not ready yet."];
|
||||
_result
|
||||
};
|
||||
|
||||
if (isNil QGVAR(MissionManager)) then {
|
||||
call FUNC(missionManager);
|
||||
};
|
||||
|
||||
if (isNil QGVAR(MissionManager)) exitWith {
|
||||
_result set ["message", "Mission manager is not ready yet."];
|
||||
_result
|
||||
};
|
||||
|
||||
GVAR(MissionManager) call ["cleanupCompletedMissions", []];
|
||||
|
||||
private _activeCount = count (GVAR(MissionManager) call ["getActiveMissionIds", []]);
|
||||
private _maxConcurrent = GVAR(MissionManager) call ["getMaxConcurrentMissions", []];
|
||||
if (_activeCount >= _maxConcurrent) exitWith {
|
||||
_result set ["message", format [
|
||||
"Mission cap reached (%1/%2 active). Close or complete a task before requesting another.",
|
||||
_activeCount,
|
||||
_maxConcurrent
|
||||
]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _generator = GVAR(MissionManager) call ["getGeneratorByType", [_generatorType]];
|
||||
if (_generator isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", format ["Generated task type is unavailable: %1", _generatorType]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _taskID = _generator call ["startMission", [GVAR(MissionManager)]];
|
||||
if (_taskID isEqualTo "") exitWith {
|
||||
_result set ["message", format ["Mission generator failed to start task type: %1", _generatorType]];
|
||||
_result
|
||||
};
|
||||
|
||||
GVAR(MissionManager) set ["lastMissionGenerationAt", diag_tickTime];
|
||||
|
||||
["INFO", format [
|
||||
"Dispatcher %1 requested generated %2 mission %3.",
|
||||
_requesterUid,
|
||||
_generatorType,
|
||||
_taskID
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", format ["Generated %1 task %2.", _generatorType, _taskID]];
|
||||
_result set ["taskID", _taskID];
|
||||
_result
|
||||
@ -1,17 +1,17 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Attack mission generator used by the dynamic mission manager.
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the Attack mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_server_task_fnc_attackMissionGenerator
|
||||
* N/A. Defines GVAR(AttackMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
@ -21,6 +21,9 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "AttackMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
|
||||
@ -85,6 +88,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
@ -104,7 +108,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_findSafePos;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
@ -134,7 +138,7 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if (!_inBlkList) then {
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
@ -161,35 +165,42 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
|
||||
if (_groups isEqualTo []) exitWith {
|
||||
["WARNING", "Attack mission generator: no AI group configs are suitable for attack missions."] call EFUNC(common,log);
|
||||
grpNull
|
||||
};
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _sideText = str _side;
|
||||
private _group = createGroup _side;
|
||||
[] call FUNC(updateEnemyCountFromActivePlayers);
|
||||
|
||||
private _groupConfig = selectRandom _groups;
|
||||
private _side = getText (_groupConfig >> "side");
|
||||
private _group = createGroup (call compile _side);
|
||||
private _minUnits = getNumber (_attackConfig >> "minUnits");
|
||||
private _maxUnits = getNumber (_attackConfig >> "maxUnits");
|
||||
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
|
||||
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
|
||||
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
|
||||
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
|
||||
|
||||
if (_minUnits <= 0) then { _minUnits = 4; };
|
||||
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
||||
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
|
||||
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
|
||||
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
|
||||
|
||||
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
|
||||
private _unitPool = [];
|
||||
{
|
||||
if ((getText (_x >> "side")) isNotEqualTo _side) then { continue; };
|
||||
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
|
||||
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
|
||||
if (_minUnits <= 0) then { _minUnits = 1; };
|
||||
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
||||
|
||||
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
|
||||
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
|
||||
{
|
||||
_unitPool pushBack createHashMapFromArray [
|
||||
["vehicle", getText (_x >> "vehicle")],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_x >> "Units"));
|
||||
} forEach _groups;
|
||||
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
|
||||
|
||||
{
|
||||
_unitPool pushBack createHashMapFromArray [
|
||||
["vehicle", getText (_x >> "vehicle")],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_x >> "Units"));
|
||||
} forEach _groups;
|
||||
};
|
||||
|
||||
if (_unitPool isEqualTo []) exitWith {
|
||||
["WARNING", format ["Attack mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call EFUNC(common,log);
|
||||
@ -301,10 +312,10 @@ GVAR(AttackMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
};
|
||||
|
||||
private _taskID = format ["task_attack_%1", round (diag_tickTime * 1000)];
|
||||
private _rewardRange = getArray (_attackConfig >> "Rewards" >> "money");
|
||||
private _reputationRange = getArray (_attackConfig >> "Rewards" >> "reputation");
|
||||
private _penaltyRange = getArray (_attackConfig >> "penalty");
|
||||
private _timeRange = getArray (_attackConfig >> "timeLimit");
|
||||
private _rewardRange = [_attackConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [25000, 60000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_attackConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [6, 14]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_attackConfig, ["penalty"], "penaltyMin", "penaltyMax", [-8, -3]] call FUNC(getMissionSettingRange);
|
||||
private _timeRange = [_attackConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
|
||||
@ -0,0 +1,446 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the HVT capture mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* N/A. Defines GVAR(CaptureHvtMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(CaptureHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "CaptureHvtMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["hvtConfig", (_missionConfig >> "MissionTypes" >> "HVTCapture")];
|
||||
_self set ["generatorType", "hvtcapture"];
|
||||
}],
|
||||
["getGeneratorType", compileFinal {
|
||||
_self getOrDefault ["generatorType", "hvtcapture"]
|
||||
}],
|
||||
["getMissionInterval", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _interval = getNumber (_missionConfig >> "missionInterval");
|
||||
if (_interval <= 0) then { _interval = 300; };
|
||||
_interval
|
||||
}],
|
||||
["getMaxConcurrentMissions", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
|
||||
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
||||
_maxConcurrent
|
||||
}],
|
||||
["getLocationReuseCooldown", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
||||
if (_cooldown <= 0) then { _cooldown = 900; };
|
||||
_cooldown
|
||||
}],
|
||||
["pruneRecentLocations", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
||||
private _now = serverTime;
|
||||
|
||||
_recentLocationRegistry = _recentLocationRegistry select {
|
||||
private _usedAt = _x param [1, -1, [0]];
|
||||
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
|
||||
};
|
||||
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
_recentLocationRegistry
|
||||
}],
|
||||
["getActiveMissionPositions", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _positions = [];
|
||||
{
|
||||
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hvtcapture") then { continue; };
|
||||
|
||||
private _position = _y getOrDefault ["position", []];
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
_positions pushBack _position;
|
||||
};
|
||||
} forEach _activeMissionRegistry;
|
||||
_positions
|
||||
}],
|
||||
["selectLocation", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
private _taskPos = [];
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 50;
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
|
||||
private _isTooClose = false;
|
||||
{
|
||||
private _prevPos = _x param [0, [], [[]]];
|
||||
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _recentLocationRegistry;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
{
|
||||
if (_candidate distance2D _x < 500) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _activeMissionPositions;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
private _inBlkList = false;
|
||||
{
|
||||
if (_candidate inArea _x) exitWith {
|
||||
_inBlkList = true;
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_taskPos isEqualTo []) exitWith {
|
||||
["WARNING", "Capture HVT mission generator: selectLocation failed to find a valid dynamic position."] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
private _building = objNull;
|
||||
private _buildingCandidates = nearestObjects [
|
||||
_taskPos,
|
||||
["House_F", "House", "Building", "BuildingBase"],
|
||||
200
|
||||
];
|
||||
if (_buildingCandidates isNotEqualTo []) then {
|
||||
_building = selectRandom _buildingCandidates;
|
||||
};
|
||||
|
||||
private _buildingPositions = [];
|
||||
if !(isNull _building) then {
|
||||
for "_i" from 0 to 100 do {
|
||||
private _buildingPos = _building buildingPos _i;
|
||||
if (_buildingPos isEqualTo [0, 0, 0]) exitWith {};
|
||||
_buildingPositions pushBack _buildingPos;
|
||||
};
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["position", _taskPos],
|
||||
["grid", mapGridPosition _taskPos],
|
||||
["buildingPositions", _buildingPositions]
|
||||
]
|
||||
}],
|
||||
|
||||
["spawnHvtTarget", compileFinal {
|
||||
params [['_position', [0, 0, 0], [[]]], ["_buildingPositions", [], [[]]]];
|
||||
|
||||
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
if (_unitPool isEqualTo []) exitWith { [] };
|
||||
|
||||
private _leaderPool = _unitPool select {
|
||||
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
|
||||
};
|
||||
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
|
||||
|
||||
private _targetDef = selectRandom _leaderPool;
|
||||
private _targetClass = _targetDef getOrDefault ["vehicle", ""];
|
||||
if (_targetClass isEqualTo "") exitWith { [] };
|
||||
|
||||
private _group = createGroup _side;
|
||||
private _leaderPos = if (_buildingPositions isEqualTo []) then {
|
||||
_position vectorAdd [(random 20 - 10), (random 20 - 10), 0]
|
||||
} else {
|
||||
selectRandom _buildingPositions
|
||||
};
|
||||
private _leader = _group createUnit [_targetClass, _leaderPos, [], 0, "NONE"];
|
||||
if (isNull _leader) exitWith {
|
||||
deleteGroup _group;
|
||||
[]
|
||||
};
|
||||
_leader setRank "LIEUTENANT";
|
||||
|
||||
[] call FUNC(updateEnemyCountFromActivePlayers);
|
||||
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
|
||||
private _escortCount = getNumber (_hvtConfig >> "escorts");
|
||||
if (_escortCount < 0) then { _escortCount = 0; };
|
||||
_escortCount = floor (_escortCount * _enemyMult);
|
||||
private _escortUnits = [];
|
||||
for "_i" from 1 to _escortCount do {
|
||||
private _escortDef = selectRandom _unitPool;
|
||||
private _escortClass = _escortDef getOrDefault ["vehicle", ""];
|
||||
if (_escortClass isEqualTo "") then { continue; };
|
||||
private _escortPos = if (_buildingPositions isEqualTo []) then {
|
||||
_position vectorAdd [(random 35 - 17), (random 35 - 17), 0]
|
||||
} else {
|
||||
selectRandom _buildingPositions
|
||||
};
|
||||
private _escort = _group createUnit [_escortClass, _escortPos, [], 0, "NONE"];
|
||||
if !(isNull _escort) then {
|
||||
_escort setRank (_escortDef getOrDefault ["rank", "PRIVATE"]);
|
||||
_escortUnits pushBack _escort;
|
||||
};
|
||||
};
|
||||
|
||||
private _groupUnits = [_leader] + _escortUnits;
|
||||
|
||||
[_group, _position, 200] call BFUNC(taskPatrol);
|
||||
|
||||
[_leader, _groupUnits]
|
||||
}],
|
||||
|
||||
["rollRewards", compileFinal {
|
||||
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
|
||||
private _equipmentRewards = [];
|
||||
private _supplyRewards = [];
|
||||
private _weaponRewards = [];
|
||||
private _vehicleRewards = [];
|
||||
private _specialRewards = [];
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
{
|
||||
_x params ["_item", "_chance"];
|
||||
if (random 1 < _chance) then {
|
||||
switch (_category) do {
|
||||
case "equipment": { _equipmentRewards pushBack _item; };
|
||||
case "supplies": { _supplyRewards pushBack _item; };
|
||||
case "weapons": { _weaponRewards pushBack _item; };
|
||||
case "vehicles": { _vehicleRewards pushBack _item; };
|
||||
case "special": { _specialRewards pushBack _item; };
|
||||
};
|
||||
};
|
||||
} forEach (getArray (_hvtConfig >> "Rewards" >> _category));
|
||||
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
||||
|
||||
createHashMapFromArray [
|
||||
["equipment", _equipmentRewards],
|
||||
["supplies", _supplyRewards],
|
||||
["weapons", _weaponRewards],
|
||||
["vehicles", _vehicleRewards],
|
||||
["special", _specialRewards]
|
||||
]
|
||||
}],
|
||||
|
||||
["startMission", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
|
||||
private _locationData = _self call ["selectLocation", [_manager]];
|
||||
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
||||
|
||||
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
||||
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
|
||||
private _buildingPositions = _locationData getOrDefault ["buildingPositions", []];
|
||||
|
||||
["INFO", format [
|
||||
"Capture HVT mission generator: selected location. Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
private _taskID = format ["task_capture_hvt_%1", round (diag_tickTime * 1000)];
|
||||
private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call FUNC(getMissionSettingRange);
|
||||
private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
|
||||
private _spawnResult = _self call ["spawnHvtTarget", [_position, _buildingPositions]];
|
||||
if !(_spawnResult isEqualType [] && { count _spawnResult >= 2 }) exitWith { "" };
|
||||
private _hvtTarget = _spawnResult select 0;
|
||||
private _hvtGroupUnits = _spawnResult select 1;
|
||||
if (isNull _hvtTarget || _hvtGroupUnits isEqualTo []) exitWith { "" };
|
||||
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
|
||||
private _timeLimit = _timeRange call BFUNC(randomNum);
|
||||
|
||||
private _extZone = format ["forge_hvt_ext_zone_%1", _taskID];
|
||||
private _extPos = [0, 0, 0];
|
||||
private _extZoneMarkers = allMapMarkers select {
|
||||
(toLowerANSI (markerText _x) find "extzone") == 0
|
||||
|| { (toLowerANSI _x find "extzone") == 0 }
|
||||
|| { (toLowerANSI (markerText _x) find "extmarker") == 0 }
|
||||
|| { (toLowerANSI _x find "extmarker") == 0 }
|
||||
};
|
||||
|
||||
if (_extZoneMarkers isNotEqualTo []) then {
|
||||
_extPos = getMarkerPos (selectRandom _extZoneMarkers);
|
||||
_extPos set [2, 0];
|
||||
} else {
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
|| { (toLowerANSI _x find "blkmarker") == 0 }
|
||||
|| { (toLowerANSI (markerText _x) find "blkmarker") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
if (_blkListMarkers isNotEqualTo []) then {
|
||||
private _selectedBlk = selectRandom _blkListMarkers;
|
||||
private _attempt = 0;
|
||||
while { _attempt < 60 && { _extPos isEqualTo [0, 0, 0] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [getMarkerPos _selectedBlk, 0, 2000, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if !(_candidate inArea _selectedBlk) then { continue; };
|
||||
_candidate set [2, 0];
|
||||
_extPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_extPos isEqualTo [0, 0, 0]) then {
|
||||
private _attempt = 0;
|
||||
while { _attempt < 80 && { _extPos isEqualTo [0, 0, 0] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _probe = [random worldSize, random worldSize, 0];
|
||||
if ((_probe distance2D _position) < 2000) then { continue; };
|
||||
private _safe = [_probe, 0, 500, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
if (_safe isEqualTo [0, 0, 0]) then { continue; };
|
||||
_safe set [2, 0];
|
||||
_extPos = _safe;
|
||||
};
|
||||
};
|
||||
|
||||
if (_extPos isEqualTo [0, 0, 0]) then {
|
||||
_extPos = _position vectorAdd [2500, 0, 0];
|
||||
_extPos set [2, 0];
|
||||
};
|
||||
};
|
||||
|
||||
createMarker [_extZone, _extPos];
|
||||
_extZone setMarkerShapeLocal "ELLIPSE";
|
||||
_extZone setMarkerSizeLocal [160, 160];
|
||||
_extZone setMarkerTextLocal format ["HVT Extraction Zone %1", _grid];
|
||||
_extZone setMarkerAlphaLocal 0.5;
|
||||
_extZone setMarkerBrushLocal "DiagGrid";
|
||||
_extZone setMarkerColor "ColorOrange";
|
||||
|
||||
private _success = [
|
||||
"hvt",
|
||||
_taskID,
|
||||
_position,
|
||||
format ["HVT: Grid %1", _grid],
|
||||
format ["Capture the high-value target near grid %1.", _grid],
|
||||
createHashMapFromArray [["hvts", [_hvtTarget]]],
|
||||
createHashMapFromArray [
|
||||
["limitFail", 0],
|
||||
["limitSuccess", 1],
|
||||
["extractionZone", _extZone],
|
||||
["captureHvt", true],
|
||||
["funds", _fundsReward],
|
||||
["ratingFail", _reputationPenalty],
|
||||
["ratingSuccess", _reputationReward],
|
||||
["endSuccess", false],
|
||||
["endFail", false],
|
||||
["timeLimit", _timeLimit],
|
||||
["equipment", _rewards get "equipment"],
|
||||
["supplies", _rewards get "supplies"],
|
||||
["weapons", _rewards get "weapons"],
|
||||
["vehicles", _rewards get "vehicles"],
|
||||
["special", _rewards get "special"]
|
||||
],
|
||||
0,
|
||||
"",
|
||||
"mission_manager"
|
||||
] call FUNC(startTask);
|
||||
|
||||
if !(_success) exitWith {
|
||||
deleteMarker _extZone;
|
||||
""
|
||||
};
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _position],
|
||||
["markers", [_extZone]],
|
||||
["startedAt", serverTime]
|
||||
]];
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
_taskID
|
||||
}],
|
||||
|
||||
["completeMission", compileFinal {
|
||||
params [
|
||||
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
|
||||
["_taskID", "", [""]]
|
||||
];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
||||
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
|
||||
|
||||
private _position = _missionRecord getOrDefault ["position", []];
|
||||
private _markers = _missionRecord getOrDefault ["markers", []];
|
||||
{
|
||||
if (_x isEqualType "" && { _x in allMapMarkers }) then {
|
||||
deleteMarker _x;
|
||||
};
|
||||
} forEach _markers;
|
||||
|
||||
_activeMissionRegistry deleteAt _taskID;
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
_recentLocationRegistry pushBack [_position, serverTime];
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
@ -0,0 +1,386 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the Defend mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* N/A. Defines GVAR(DefendMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(DefendMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "DefendMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["defendConfig", (_missionConfig >> "MissionTypes" >> "Defend")];
|
||||
_self set ["generatorType", "defend"];
|
||||
}],
|
||||
["getGeneratorType", compileFinal {
|
||||
_self getOrDefault ["generatorType", "defend"]
|
||||
}],
|
||||
["getMissionInterval", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _interval = getNumber (_missionConfig >> "missionInterval");
|
||||
if (_interval <= 0) then { _interval = 300; };
|
||||
_interval
|
||||
}],
|
||||
["getMaxConcurrentMissions", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
|
||||
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
||||
_maxConcurrent
|
||||
}],
|
||||
["getLocationReuseCooldown", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
||||
if (_cooldown <= 0) then { _cooldown = 900; };
|
||||
_cooldown
|
||||
}],
|
||||
["pruneRecentLocations", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
||||
private _now = serverTime;
|
||||
|
||||
_recentLocationRegistry = _recentLocationRegistry select {
|
||||
private _usedAt = _x param [1, -1, [0]];
|
||||
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
|
||||
};
|
||||
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
_recentLocationRegistry
|
||||
}],
|
||||
["getActiveMissionPositions", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _positions = [];
|
||||
{
|
||||
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "defend") then { continue; };
|
||||
|
||||
private _position = _y getOrDefault ["position", []];
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
_positions pushBack _position;
|
||||
};
|
||||
} forEach _activeMissionRegistry;
|
||||
_positions
|
||||
}],
|
||||
["selectLocation", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
private _taskPos = [];
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 50;
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
|
||||
private _isTooClose = false;
|
||||
{
|
||||
private _prevPos = _x param [0, [], [[]]];
|
||||
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _recentLocationRegistry;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
{
|
||||
if (_candidate distance2D _x < 500) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _activeMissionPositions;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
private _inBlkList = false;
|
||||
{
|
||||
if (_candidate inArea _x) exitWith {
|
||||
_inBlkList = true;
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_taskPos isEqualTo []) exitWith {
|
||||
["WARNING", "Defend mission generator: selectLocation failed to find a valid dynamic position."] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["position", _taskPos],
|
||||
["grid", mapGridPosition _taskPos]
|
||||
]
|
||||
}],
|
||||
|
||||
["buildDefendTemplateGroups", compileFinal {
|
||||
params [['_position', [0, 0, 0], [[]]]];
|
||||
|
||||
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
|
||||
private _defendConfig = _self getOrDefault ["defendConfig", configNull];
|
||||
private _groups = [];
|
||||
|
||||
{
|
||||
if ("defend" in getArray (_x >> "suitable")) then {
|
||||
_groups pushBack _x;
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
|
||||
if (_groups isEqualTo []) then {
|
||||
{
|
||||
if ("attack" in getArray (_x >> "suitable")) then {
|
||||
_groups pushBack _x;
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
};
|
||||
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _sideText = str _side;
|
||||
[] call FUNC(updateEnemyCountFromActivePlayers);
|
||||
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
|
||||
private _unitCountConfig = getArray (_defendConfig >> "unitsPerWave");
|
||||
private _minUnits = _unitCountConfig select 0;
|
||||
private _maxUnits = _unitCountConfig select 1;
|
||||
if (_minUnits <= 0) then { _minUnits = 4; };
|
||||
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
||||
_minUnits = floor ((_minUnits max 1) * _enemyMult);
|
||||
_maxUnits = ceil ((_maxUnits max _minUnits) * _enemyMult);
|
||||
if (_minUnits <= 0) then { _minUnits = 1; };
|
||||
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
||||
private _targetUnitCount = _minUnits + floor random ((_maxUnits - _minUnits) + 1);
|
||||
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
|
||||
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
|
||||
{
|
||||
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
|
||||
|
||||
{
|
||||
_unitPool pushBack createHashMapFromArray [
|
||||
["vehicle", getText (_x >> "vehicle")],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_x >> "Units"));
|
||||
} forEach _groups;
|
||||
};
|
||||
|
||||
if (_unitPool isEqualTo []) exitWith { [] };
|
||||
|
||||
private _templateGroup = [];
|
||||
for "_i" from 1 to _targetUnitCount do {
|
||||
private _unitDef = selectRandom _unitPool;
|
||||
private _unitClass = _unitDef getOrDefault ["vehicle", ""];
|
||||
if (_unitClass isNotEqualTo "") then {
|
||||
_templateGroup pushBack createHashMapFromArray [
|
||||
["type", _unitClass],
|
||||
["side", _side],
|
||||
["rank", _unitDef getOrDefault ["rank", "PRIVATE"]],
|
||||
["skill", 0.45 + random 0.25]
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
if (_templateGroup isEqualTo []) exitWith { [] };
|
||||
[_templateGroup]
|
||||
}],
|
||||
|
||||
["rollRewards", compileFinal {
|
||||
private _defendConfig = _self getOrDefault ["defendConfig", configNull];
|
||||
private _equipmentRewards = [];
|
||||
private _supplyRewards = [];
|
||||
private _weaponRewards = [];
|
||||
private _vehicleRewards = [];
|
||||
private _specialRewards = [];
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
{
|
||||
_x params ["_item", "_chance"];
|
||||
if (random 1 < _chance) then {
|
||||
switch (_category) do {
|
||||
case "equipment": { _equipmentRewards pushBack _item; };
|
||||
case "supplies": { _supplyRewards pushBack _item; };
|
||||
case "weapons": { _weaponRewards pushBack _item; };
|
||||
case "vehicles": { _vehicleRewards pushBack _item; };
|
||||
case "special": { _specialRewards pushBack _item; };
|
||||
};
|
||||
};
|
||||
} forEach (getArray (_defendConfig >> "Rewards" >> _category));
|
||||
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
||||
|
||||
createHashMapFromArray [
|
||||
["equipment", _equipmentRewards],
|
||||
["supplies", _supplyRewards],
|
||||
["weapons", _weaponRewards],
|
||||
["vehicles", _vehicleRewards],
|
||||
["special", _specialRewards]
|
||||
]
|
||||
}],
|
||||
|
||||
["startMission", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _defendConfig = _self getOrDefault ["defendConfig", configNull];
|
||||
private _locationData = _self call ["selectLocation", [_manager]];
|
||||
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
||||
|
||||
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
||||
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
|
||||
|
||||
private _taskID = format ["task_defend_%1", round (diag_tickTime * 1000)];
|
||||
private _rewardRange = [_defendConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [40000, 90000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_defendConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [8, 18]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_defendConfig, ["penalty"], "penaltyMin", "penaltyMax", [-12, -4]] call FUNC(getMissionSettingRange);
|
||||
private _timeRange = [_defendConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [300, 1800]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
private _enemyTemplates = _self call ["buildDefendTemplateGroups", [_position]];
|
||||
if (_enemyTemplates isEqualTo []) exitWith { "" };
|
||||
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
|
||||
private _timeLimit = _timeRange call BFUNC(randomNum);
|
||||
|
||||
private _minWaves = getNumber (_defendConfig >> "minWaves");
|
||||
if (_minWaves <= 0) then { _minWaves = 3; };
|
||||
private _maxWaves = getNumber (_defendConfig >> "maxWaves");
|
||||
if (_maxWaves < _minWaves) then { _maxWaves = _minWaves; };
|
||||
private _limitSuccess = _minWaves + floor random ((_maxWaves - _minWaves) + 1);
|
||||
private _waveCooldown = getNumber (_defendConfig >> "waveCooldown");
|
||||
if (_waveCooldown <= 0) then { _waveCooldown = 300; };
|
||||
private _minBlufor = 1;
|
||||
|
||||
private _defenseZone = format ["forge_defend_zone_%1", _taskID];
|
||||
createMarker [_defenseZone, _position];
|
||||
_defenseZone setMarkerShapeLocal "ELLIPSE";
|
||||
_defenseZone setMarkerSizeLocal [25, 25];
|
||||
_defenseZone setMarkerTextLocal format ["Defense Zone %1", _grid];
|
||||
_defenseZone setMarkerAlphaLocal 0.5;
|
||||
_defenseZone setMarkerBrushLocal "DiagGrid";
|
||||
_defenseZone setMarkerColor "ColorOrange";
|
||||
|
||||
private _success = [
|
||||
"defend",
|
||||
_taskID,
|
||||
_position,
|
||||
format ["Defend: Grid %1", _grid],
|
||||
format ["Hold the area in and around grid %1.", _grid],
|
||||
createHashMapFromArray [],
|
||||
createHashMapFromArray [
|
||||
["limitFail", 0],
|
||||
["limitSuccess", _limitSuccess],
|
||||
["funds", _fundsReward],
|
||||
["ratingFail", _reputationPenalty],
|
||||
["ratingSuccess", _reputationReward],
|
||||
["endSuccess", false],
|
||||
["endFail", false],
|
||||
["timeLimit", _timeLimit],
|
||||
["equipment", _rewards get "equipment"],
|
||||
["supplies", _rewards get "supplies"],
|
||||
["weapons", _rewards get "weapons"],
|
||||
["vehicles", _rewards get "vehicles"],
|
||||
["special", _rewards get "special"],
|
||||
["defenseZone", _defenseZone],
|
||||
["defendTime", _timeLimit],
|
||||
["waveCount", _limitSuccess],
|
||||
["waveCooldown", _waveCooldown],
|
||||
["minBlufor", _minBlufor],
|
||||
["enemyTemplates", _enemyTemplates]
|
||||
],
|
||||
0,
|
||||
"",
|
||||
"mission_manager"
|
||||
] call FUNC(startTask);
|
||||
|
||||
if !(_success) exitWith {
|
||||
deleteMarker _defenseZone;
|
||||
""
|
||||
};
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _position],
|
||||
["markers", [_defenseZone]],
|
||||
["startedAt", serverTime]
|
||||
]];
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
_taskID
|
||||
}],
|
||||
|
||||
["completeMission", compileFinal {
|
||||
params [
|
||||
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
|
||||
["_taskID", "", [""]]
|
||||
];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
||||
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
|
||||
|
||||
private _position = _missionRecord getOrDefault ["position", []];
|
||||
private _markers = _missionRecord getOrDefault ["markers", []];
|
||||
{
|
||||
if (_x isEqualType "" && { _x in allMapMarkers }) then {
|
||||
deleteMarker _x;
|
||||
};
|
||||
} forEach _markers;
|
||||
|
||||
_activeMissionRegistry deleteAt _taskID;
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
_recentLocationRegistry pushBack [_position, serverTime];
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
@ -0,0 +1,513 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the Defuse mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* N/A. Defines GVAR(DefuseMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(DefuseMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "DefuseMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
|
||||
_self set ["defuseConfig", (_missionConfig >> "MissionTypes" >> "Defuse")];
|
||||
_self set ["generatorType", "defuse"];
|
||||
}],
|
||||
["getGeneratorType", compileFinal {
|
||||
_self getOrDefault ["generatorType", "defuse"]
|
||||
}],
|
||||
["getMissionInterval", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _interval = getNumber (_missionConfig >> "missionInterval");
|
||||
if (_interval <= 0) then { _interval = 300; };
|
||||
_interval
|
||||
}],
|
||||
["getMaxConcurrentMissions", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
|
||||
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
||||
_maxConcurrent
|
||||
}],
|
||||
["getLocationReuseCooldown", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
||||
if (_cooldown <= 0) then { _cooldown = 900; };
|
||||
_cooldown
|
||||
}],
|
||||
["pruneRecentLocations", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
||||
private _now = serverTime;
|
||||
|
||||
_recentLocationRegistry = _recentLocationRegistry select {
|
||||
private _usedAt = _x param [1, -1, [0]];
|
||||
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
|
||||
};
|
||||
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
_recentLocationRegistry
|
||||
}],
|
||||
["getActiveMissionPositions", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _positions = [];
|
||||
{
|
||||
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "defuse") then { continue; };
|
||||
|
||||
private _position = _y getOrDefault ["position", []];
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
_positions pushBack _position;
|
||||
};
|
||||
} forEach _activeMissionRegistry;
|
||||
_positions
|
||||
}],
|
||||
["selectLocation", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
private _taskPos = [];
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 50;
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
|
||||
private _isTooClose = false;
|
||||
{
|
||||
private _prevPos = _x param [0, [], [[]]];
|
||||
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _recentLocationRegistry;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
{
|
||||
if (_candidate distance2D _x < 500) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _activeMissionPositions;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
private _inBlkList = false;
|
||||
{
|
||||
if (_candidate inArea _x) exitWith {
|
||||
_inBlkList = true;
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_taskPos isEqualTo []) exitWith {
|
||||
["WARNING", "Defuse mission generator: selectLocation failed to find a valid dynamic position."] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["position", _taskPos],
|
||||
["grid", mapGridPosition _taskPos]
|
||||
]
|
||||
}],
|
||||
|
||||
["spawnPatrolGroup", compileFinal {
|
||||
params [["_position", [0, 0, 0], [[]]]];
|
||||
|
||||
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
|
||||
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
|
||||
private _groups = [];
|
||||
{
|
||||
if ("attack" in getArray (_x >> "suitable")) then {
|
||||
_groups pushBack _x;
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _sideText = str _side;
|
||||
private _group = createGroup _side;
|
||||
[] call FUNC(updateEnemyCountFromActivePlayers);
|
||||
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
|
||||
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
|
||||
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
|
||||
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
|
||||
|
||||
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
|
||||
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
|
||||
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
|
||||
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
|
||||
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
|
||||
|
||||
if (_minUnits <= 0) then { _minUnits = 1; };
|
||||
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
||||
|
||||
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
|
||||
diag_log format ["Defuse: Unit Count %1", _targetUnitCount];
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
|
||||
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
|
||||
{
|
||||
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
|
||||
|
||||
{
|
||||
_unitPool pushBack createHashMapFromArray [
|
||||
["vehicle", getText (_x >> "vehicle")],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_x >> "Units"));
|
||||
} forEach _groups;
|
||||
};
|
||||
|
||||
if (_unitPool isEqualTo []) exitWith {
|
||||
["WARNING", format ["Defuse mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call EFUNC(common,log);
|
||||
deleteGroup _group;
|
||||
grpNull
|
||||
};
|
||||
|
||||
private _leaderPool = _unitPool select {
|
||||
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
|
||||
};
|
||||
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
|
||||
|
||||
private _spawnDefs = [selectRandom _leaderPool];
|
||||
for "_i" from 1 to (_targetUnitCount - 1) do {
|
||||
_spawnDefs pushBack (selectRandom _unitPool);
|
||||
};
|
||||
|
||||
{
|
||||
private _unitClass = _x getOrDefault ["vehicle", ""];
|
||||
if (_unitClass isEqualTo "") then { continue; };
|
||||
|
||||
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
|
||||
if (count _unitOffset < 3) then { _unitOffset resize 3; };
|
||||
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
|
||||
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
|
||||
|
||||
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
|
||||
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
|
||||
} forEach _spawnDefs;
|
||||
|
||||
[_group, _position, _patrolRadius] call BFUNC(taskPatrol);
|
||||
|
||||
["INFO", format [
|
||||
"Defuse mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4",
|
||||
_side,
|
||||
count (units _group),
|
||||
_patrolRadius,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
_group
|
||||
}],
|
||||
|
||||
["rollRewards", compileFinal {
|
||||
private _defuseConfig = _self getOrDefault ["defuseConfig", configNull];
|
||||
private _equipmentRewards = [];
|
||||
private _supplyRewards = [];
|
||||
private _weaponRewards = [];
|
||||
private _vehicleRewards = [];
|
||||
private _specialRewards = [];
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
{
|
||||
_x params ["_item", "_chance"];
|
||||
if (random 1 < _chance) then {
|
||||
switch (_category) do {
|
||||
case "equipment": { _equipmentRewards pushBack _item; };
|
||||
case "supplies": { _supplyRewards pushBack _item; };
|
||||
case "weapons": { _weaponRewards pushBack _item; };
|
||||
case "vehicles": { _vehicleRewards pushBack _item; };
|
||||
case "special": { _specialRewards pushBack _item; };
|
||||
};
|
||||
};
|
||||
} forEach (getArray (_defuseConfig >> "Rewards" >> _category));
|
||||
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
||||
|
||||
createHashMapFromArray [
|
||||
["equipment", _equipmentRewards],
|
||||
["supplies", _supplyRewards],
|
||||
["weapons", _weaponRewards],
|
||||
["vehicles", _vehicleRewards],
|
||||
["special", _specialRewards]
|
||||
]
|
||||
}],
|
||||
|
||||
["spawnDefuseDevices", compileFinal {
|
||||
params [['_position', [0, 0, 0], [[]]]];
|
||||
|
||||
private _defuseConfig = _self getOrDefault ["defuseConfig", configNull];
|
||||
private _smallDevices = getArray (_defuseConfig >> "Devices" >> "small");
|
||||
private _largeDevices = getArray (_defuseConfig >> "Devices" >> "large");
|
||||
private _protectedClasses = getArray (_defuseConfig >> "Devices" >> "protected");
|
||||
private _devicePool = _smallDevices + _largeDevices;
|
||||
if (_devicePool isEqualTo [] || _protectedClasses isEqualTo []) exitWith { [] };
|
||||
|
||||
private _maxDevices = getNumber (_defuseConfig >> "maxDevices");
|
||||
if (_maxDevices <= 0) then { _maxDevices = 1; };
|
||||
private _deviceCount = 1 + floor (random _maxDevices);
|
||||
|
||||
private _protectedClass = selectRandom _protectedClasses;
|
||||
|
||||
// Try to spawn inside a building if there is a suitable building near the selected location.
|
||||
// This will attempt up to N building positions before falling back to outdoor offsets.
|
||||
private _buildingSpawnAttempts = 10;
|
||||
private _buildingPos = [];
|
||||
|
||||
private _nearBuildings = nearestObjects [_position, ["House"], 50];
|
||||
private _building = objNull;
|
||||
if (_nearBuildings isNotEqualTo []) then {
|
||||
// prefer the closest building that actually contains the position
|
||||
{
|
||||
if !(isNull _x && { _position inArea _x }) exitWith {
|
||||
_building = _x;
|
||||
};
|
||||
} forEach _nearBuildings;
|
||||
|
||||
if (isNull _building) then {
|
||||
// fallback: pick nearest
|
||||
_building = _nearBuildings select 0;
|
||||
{
|
||||
if (_position distance2D _x < _position distance2D _building) then {
|
||||
_building = _x;
|
||||
};
|
||||
} forEach _nearBuildings;
|
||||
};
|
||||
};
|
||||
|
||||
if !(isNull _building) then {
|
||||
for "_i" from 1 to _buildingSpawnAttempts do {
|
||||
private _posIndex = floor random 1000;
|
||||
private _candidate = _building buildingPos _posIndex;
|
||||
// buildingPos returns [0,0,0] for invalid positions
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
// ensure candidate is still inside the building footprint
|
||||
if !((_candidate isEqualType [])) then { continue; };
|
||||
if ((_candidate vectorDistance _position) <= 60) exitWith {
|
||||
_buildingPos = _candidate;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
private _protectedPos = [0,0,0];
|
||||
if (_buildingPos isNotEqualTo []) then {
|
||||
_protectedPos = _buildingPos;
|
||||
} else {
|
||||
// Outdoor fallback: keep previous behavior
|
||||
_protectedPos = _position vectorAdd [(random 20 - 10), (random 20 - 10), 0];
|
||||
};
|
||||
|
||||
private _protectedObject = createVehicle [_protectedClass, _protectedPos, [], 0, "NONE"];
|
||||
private _protectedObjects = [];
|
||||
if !(isNull _protectedObject) then {
|
||||
_protectedObjects pushBack _protectedObject;
|
||||
};
|
||||
|
||||
private _deviceRadiusMin = 2;
|
||||
private _deviceRadiusMax = 5;
|
||||
private _devices = [];
|
||||
|
||||
for "_i" from 1 to _deviceCount do {
|
||||
private _deviceClass = selectRandom _devicePool;
|
||||
|
||||
// If we managed to pick a building position, keep devices clustered relative to it.
|
||||
// This keeps them inside the building volume more reliably than using ground offsets.
|
||||
private _angle = random 2 * pi;
|
||||
private _radius = _deviceRadiusMin + random (_deviceRadiusMax - _deviceRadiusMin);
|
||||
private _deviceOffset = [_radius * cos _angle, _radius * sin _angle, 0];
|
||||
private _devicePos = _protectedPos vectorAdd _deviceOffset;
|
||||
|
||||
private _deviceObject = createVehicle [_deviceClass, _devicePos, [], 0, "NONE"];
|
||||
if !(isNull _deviceObject) then {
|
||||
_devices pushBack _deviceObject;
|
||||
};
|
||||
};
|
||||
|
||||
[_devices, _protectedObjects]
|
||||
}],
|
||||
|
||||
["startMission", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _defuseConfig = _self getOrDefault ["defuseConfig", configNull];
|
||||
private _locationData = _self call ["selectLocation", [_manager]];
|
||||
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
||||
|
||||
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
||||
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
|
||||
|
||||
["INFO", format [
|
||||
"Defuse mission generator: selected location. Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
private _group = _self call ["spawnPatrolGroup", [_position]];
|
||||
if (isNull _group) exitWith {
|
||||
["WARNING", format [
|
||||
"Defuse mission generator: spawnPatrolGroup failed for Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
""
|
||||
};
|
||||
|
||||
private _units = units _group;
|
||||
if (_units isEqualTo []) exitWith {
|
||||
["WARNING", format [
|
||||
"Defuse mission generator: spawned group has no units. Grid=%1, Group=%2",
|
||||
_grid,
|
||||
_group
|
||||
]] call EFUNC(common,log);
|
||||
deleteGroup _group;
|
||||
""
|
||||
};
|
||||
|
||||
private _taskID = format ["task_defuse_%1", round (diag_tickTime * 1000)];
|
||||
private _rewardRange = [_defuseConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [20000, 50000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_defuseConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [5, 12]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_defuseConfig, ["penalty"], "penaltyMin", "penaltyMax", [-9, -3]] call FUNC(getMissionSettingRange);
|
||||
private _timeRange = [_defuseConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
|
||||
private _spawnResult = _self call ["spawnDefuseDevices", [_position]];
|
||||
private _devices = _spawnResult select 0;
|
||||
private _protectedObjects = _spawnResult select 1;
|
||||
if (_devices isEqualTo [] || _protectedObjects isEqualTo []) exitWith { "" };
|
||||
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
|
||||
private _timeLimit = _timeRange call BFUNC(randomNum);
|
||||
private _iedTimer = 300;
|
||||
private _targetCount = count _devices;
|
||||
|
||||
private _defuseZone = format ["forge_defuse_zone_%1", _taskID];
|
||||
createMarker [_defuseZone, _position];
|
||||
_defuseZone setMarkerShapeLocal "ELLIPSE";
|
||||
_defuseZone setMarkerSizeLocal [120, 120];
|
||||
_defuseZone setMarkerText format ["Defuse Area %1", _grid];
|
||||
|
||||
private _success = [
|
||||
"defuse",
|
||||
_taskID,
|
||||
_position,
|
||||
format ["Defuse: Grid %1", _grid],
|
||||
format ["Defuse explosives operating near grid %1.", _grid],
|
||||
createHashMapFromArray [["ieds", _devices], ["protected", _protectedObjects]],
|
||||
createHashMapFromArray [
|
||||
["limitFail", 0],
|
||||
["limitSuccess", _targetCount],
|
||||
["funds", _fundsReward],
|
||||
["ratingFail", _reputationPenalty],
|
||||
["ratingSuccess", _reputationReward],
|
||||
["endSuccess", false],
|
||||
["endFail", false],
|
||||
["timeLimit", _timeLimit],
|
||||
["equipment", _rewards get "equipment"],
|
||||
["supplies", _rewards get "supplies"],
|
||||
["weapons", _rewards get "weapons"],
|
||||
["vehicles", _rewards get "vehicles"],
|
||||
["special", _rewards get "special"],
|
||||
["iedTimer", _iedTimer],
|
||||
["defuseZone", _defuseZone]
|
||||
],
|
||||
0,
|
||||
"",
|
||||
"mission_manager"
|
||||
] call FUNC(startTask);
|
||||
|
||||
if !(_success) exitWith {
|
||||
deleteMarker _defuseZone;
|
||||
""
|
||||
};
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _position],
|
||||
["markers", [_defuseZone]],
|
||||
["startedAt", serverTime]
|
||||
]];
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
_taskID
|
||||
}],
|
||||
|
||||
["completeMission", compileFinal {
|
||||
params [
|
||||
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
|
||||
["_taskID", "", [""]]
|
||||
];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
||||
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
|
||||
|
||||
private _position = _missionRecord getOrDefault ["position", []];
|
||||
private _markers = _missionRecord getOrDefault ["markers", []];
|
||||
{
|
||||
if (_x isEqualType "" && { _x in allMapMarkers }) then {
|
||||
deleteMarker _x;
|
||||
};
|
||||
} forEach _markers;
|
||||
|
||||
_activeMissionRegistry deleteAt _taskID;
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
_recentLocationRegistry pushBack [_position, serverTime];
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
@ -0,0 +1,378 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the Delivery mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* N/A. Defines GVAR(DeliveryMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(DeliveryMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "DeliveryMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["deliveryConfig", (_missionConfig >> "MissionTypes" >> "Delivery")];
|
||||
_self set ["generatorType", "delivery"];
|
||||
}],
|
||||
["getGeneratorType", compileFinal {
|
||||
_self getOrDefault ["generatorType", "delivery"]
|
||||
}],
|
||||
["getMissionInterval", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _interval = getNumber (_missionConfig >> "missionInterval");
|
||||
if (_interval <= 0) then { _interval = 300; };
|
||||
_interval
|
||||
}],
|
||||
["getMaxConcurrentMissions", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
|
||||
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
||||
_maxConcurrent
|
||||
}],
|
||||
["getLocationReuseCooldown", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
||||
if (_cooldown <= 0) then { _cooldown = 900; };
|
||||
_cooldown
|
||||
}],
|
||||
["pruneRecentLocations", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
||||
private _now = serverTime;
|
||||
|
||||
_recentLocationRegistry = _recentLocationRegistry select {
|
||||
private _usedAt = _x param [1, -1, [0]];
|
||||
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
|
||||
};
|
||||
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
_recentLocationRegistry
|
||||
}],
|
||||
["getActiveMissionPositions", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _positions = [];
|
||||
{
|
||||
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "delivery") then { continue; };
|
||||
|
||||
private _position = _y getOrDefault ["position", []];
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
_positions pushBack _position;
|
||||
};
|
||||
} forEach _activeMissionRegistry;
|
||||
_positions
|
||||
}],
|
||||
["selectLocation", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
private _taskPos = [];
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 50;
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
|
||||
private _isTooClose = false;
|
||||
{
|
||||
private _prevPos = _x param [0, [], [[]]];
|
||||
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _recentLocationRegistry;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
{
|
||||
if (_candidate distance2D _x < 500) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _activeMissionPositions;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
private _inBlkList = false;
|
||||
{
|
||||
if (_candidate inArea _x) exitWith {
|
||||
_inBlkList = true;
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_taskPos isEqualTo []) exitWith {
|
||||
["WARNING", "Delivery mission generator: selectLocation failed to find a valid dynamic position."] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["position", _taskPos],
|
||||
["grid", mapGridPosition _taskPos]
|
||||
]
|
||||
}],
|
||||
|
||||
["rollRewards", compileFinal {
|
||||
private _deliveryConfig = _self getOrDefault ["deliveryConfig", configNull];
|
||||
private _equipmentRewards = [];
|
||||
private _supplyRewards = [];
|
||||
private _weaponRewards = [];
|
||||
private _vehicleRewards = [];
|
||||
private _specialRewards = [];
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
{
|
||||
_x params ["_item", "_chance"];
|
||||
if (random 1 < _chance) then {
|
||||
switch (_category) do {
|
||||
case "equipment": { _equipmentRewards pushBack _item; };
|
||||
case "supplies": { _supplyRewards pushBack _item; };
|
||||
case "weapons": { _weaponRewards pushBack _item; };
|
||||
case "vehicles": { _vehicleRewards pushBack _item; };
|
||||
case "special": { _specialRewards pushBack _item; };
|
||||
};
|
||||
};
|
||||
} forEach (getArray (_deliveryConfig >> "Rewards" >> _category));
|
||||
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
||||
|
||||
createHashMapFromArray [
|
||||
["equipment", _equipmentRewards],
|
||||
["supplies", _supplyRewards],
|
||||
["weapons", _weaponRewards],
|
||||
["vehicles", _vehicleRewards],
|
||||
["special", _specialRewards]
|
||||
]
|
||||
}],
|
||||
|
||||
["getCargoPickupPosition", compileFinal {
|
||||
params [["_fallbackPosition", [0, 0, 0], [[]]]];
|
||||
|
||||
if ("CargoSpawn" in allMapMarkers) exitWith { getMarkerPos "CargoSpawn" };
|
||||
|
||||
private _cargoSpawn = missionNamespace getVariable ["CargoSpawn", objNull];
|
||||
if (_cargoSpawn isEqualType "" && { _cargoSpawn in allMapMarkers }) exitWith { getMarkerPos _cargoSpawn };
|
||||
if (_cargoSpawn isEqualType objNull && { !(isNull _cargoSpawn) }) exitWith { getPosATL _cargoSpawn };
|
||||
|
||||
if ("ExtZone" in allMapMarkers) exitWith { getMarkerPos "ExtZone" };
|
||||
|
||||
private _extZone = missionNamespace getVariable ["ExtZone", objNull];
|
||||
if (_extZone isEqualType "" && { _extZone in allMapMarkers }) exitWith { getMarkerPos _extZone };
|
||||
if (_extZone isEqualType objNull && { !(isNull _extZone) }) exitWith { getPosATL _extZone };
|
||||
|
||||
_fallbackPosition
|
||||
}],
|
||||
|
||||
["spawnDeliveryCargo", compileFinal {
|
||||
params [["_pickupPosition", [0, 0, 0], [[]]]];
|
||||
|
||||
private _deliveryConfig = _self getOrDefault ["deliveryConfig", configNull];
|
||||
private _supplyCargo = getArray (_deliveryConfig >> "Cargo" >> "supplies");
|
||||
private _vehicleCargo = getArray (_deliveryConfig >> "Cargo" >> "vehicles");
|
||||
private _cargoPool = _supplyCargo + _vehicleCargo;
|
||||
private _cargoCount = 1 + floor (random 2);
|
||||
private _cargoObjects = [];
|
||||
|
||||
if (_cargoPool isEqualTo []) exitWith { [] };
|
||||
|
||||
for "_i" from 1 to _cargoCount do {
|
||||
private _cargoClass = selectRandom _cargoPool;
|
||||
private _spawnPos = _pickupPosition vectorAdd [(random 12 - 6), (random 12 - 6), 0];
|
||||
|
||||
private _cargoObject = createVehicle [_cargoClass, _spawnPos, [], 0, "NONE"];
|
||||
if !(isNull _cargoObject) then {
|
||||
_cargoObjects pushBack _cargoObject;
|
||||
};
|
||||
};
|
||||
|
||||
_cargoObjects
|
||||
}],
|
||||
|
||||
["startMission", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _deliveryConfig = _self getOrDefault ["deliveryConfig", configNull];
|
||||
private _locationData = _self call ["selectLocation", [_manager]];
|
||||
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
||||
|
||||
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
||||
private _pickupPos = _self call ["getCargoPickupPosition", [_position]];
|
||||
private _grid = mapGridPosition _pickupPos;
|
||||
private _taskID = format ["task_delivery_%1", round (diag_tickTime * 1000)];
|
||||
private _pickupMarker = format ["forge_delivery_pickup_%1", _taskID];
|
||||
private _deliveryZone = format ["forge_delivery_zone_%1", _taskID];
|
||||
private _dropoffMarker = format ["forge_delivery_dropoff_%1", _taskID];
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _deliveryPos = [0, 0, 0];
|
||||
private _attempt = 0;
|
||||
private _deliverySearchRadius = (_worldSize / 2 - 1000) max 500;
|
||||
while { _attempt < 80 && { _deliveryPos isEqualTo [0, 0, 0] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, 0, _deliverySearchRadius, 10, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if ((_candidate distance2D _pickupPos) < 1200) then { continue; };
|
||||
_candidate set [2, 0];
|
||||
_deliveryPos = _candidate;
|
||||
};
|
||||
if (_deliveryPos isEqualTo [0, 0, 0]) then {
|
||||
_deliveryPos = [_pickupPos, 1200, 2500, 10, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
};
|
||||
if (_deliveryPos isEqualTo [0, 0, 0]) then {
|
||||
_deliveryPos = _pickupPos vectorAdd [1500, 0, 0];
|
||||
};
|
||||
private _deliveryGrid = mapGridPosition _deliveryPos;
|
||||
|
||||
private _rewardRange = [_deliveryConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [10000, 30000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_deliveryConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_deliveryConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
private _cargoObjects = _self call ["spawnDeliveryCargo", [_pickupPos]];
|
||||
|
||||
if (_cargoObjects isEqualTo []) exitWith { "" };
|
||||
|
||||
createMarker [_pickupMarker, _pickupPos];
|
||||
_pickupMarker setMarkerTypeLocal "hd_pickup";
|
||||
_pickupMarker setMarkerColorLocal "ColorBLUFOR";
|
||||
_pickupMarker setMarkerText format ["Pickup %1", _grid];
|
||||
|
||||
createMarker [_deliveryZone, _deliveryPos];
|
||||
_deliveryZone setMarkerShapeLocal "ELLIPSE";
|
||||
_deliveryZone setMarkerSizeLocal [25, 25];
|
||||
_deliveryZone setMarkerTextLocal format ["Delivery Zone %1", _deliveryGrid];
|
||||
_deliveryZone setMarkerAlphaLocal 0.5;
|
||||
_deliveryZone setMarkerBrushLocal "DiagGrid";
|
||||
_deliveryZone setMarkerColor "ColorOrange";
|
||||
|
||||
createMarker [_dropoffMarker, _deliveryPos];
|
||||
_dropoffMarker setMarkerTypeLocal "hd_end";
|
||||
_dropoffMarker setMarkerColorLocal "ColorBLUFOR";
|
||||
_dropoffMarker setMarkerText format ["Drop-off %1", _deliveryGrid];
|
||||
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
|
||||
private _timeLimit = 0;
|
||||
private _cargoCount = count _cargoObjects;
|
||||
|
||||
private _success = [
|
||||
"delivery",
|
||||
_taskID,
|
||||
_pickupPos,
|
||||
format ["Delivery: Grid %1", _grid],
|
||||
format ["Move cargo from grid %1 to the delivery zone near grid %2.", _grid, _deliveryGrid],
|
||||
createHashMapFromArray [["cargo", _cargoObjects]],
|
||||
createHashMapFromArray [
|
||||
["limitFail", 1],
|
||||
["limitSuccess", _cargoCount],
|
||||
["deliveryZone", _deliveryZone],
|
||||
["funds", _fundsReward],
|
||||
["ratingFail", _reputationPenalty],
|
||||
["ratingSuccess", _reputationReward],
|
||||
["endSuccess", false],
|
||||
["endFail", false],
|
||||
["timeLimit", _timeLimit],
|
||||
["equipment", _rewards get "equipment"],
|
||||
["supplies", _rewards get "supplies"],
|
||||
["weapons", _rewards get "weapons"],
|
||||
["vehicles", _rewards get "vehicles"],
|
||||
["special", _rewards get "special"]
|
||||
],
|
||||
0,
|
||||
"",
|
||||
"mission_manager"
|
||||
] call FUNC(startTask);
|
||||
|
||||
if !(_success) exitWith {
|
||||
deleteMarker _pickupMarker;
|
||||
deleteMarker _deliveryZone;
|
||||
deleteMarker _dropoffMarker;
|
||||
""
|
||||
};
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _pickupPos],
|
||||
["markers", [_pickupMarker, _deliveryZone, _dropoffMarker]],
|
||||
["startedAt", serverTime]
|
||||
]];
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
_taskID
|
||||
}],
|
||||
|
||||
["completeMission", compileFinal {
|
||||
params [
|
||||
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
|
||||
["_taskID", "", [""]]
|
||||
];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
||||
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
|
||||
|
||||
private _position = _missionRecord getOrDefault ["position", []];
|
||||
private _markers = _missionRecord getOrDefault ["markers", []];
|
||||
{
|
||||
if (_x isEqualType "" && { _x in allMapMarkers }) then {
|
||||
deleteMarker _x;
|
||||
};
|
||||
} forEach _markers;
|
||||
|
||||
_activeMissionRegistry deleteAt _taskID;
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
_recentLocationRegistry pushBack [_position, serverTime];
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
@ -0,0 +1,469 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the Destroy mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* N/A. Defines GVAR(DestroyMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(DestroyMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "DestroyMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
|
||||
_self set ["destroyConfig", (_missionConfig >> "MissionTypes" >> "Destroy")];
|
||||
_self set ["generatorType", "destroy"];
|
||||
}],
|
||||
["getGeneratorType", compileFinal {
|
||||
_self getOrDefault ["generatorType", "destroy"]
|
||||
}],
|
||||
["getMissionInterval", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _interval = getNumber (_missionConfig >> "missionInterval");
|
||||
if (_interval <= 0) then { _interval = 300; };
|
||||
_interval
|
||||
}],
|
||||
["getMaxConcurrentMissions", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
|
||||
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
||||
_maxConcurrent
|
||||
}],
|
||||
["getLocationReuseCooldown", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
||||
if (_cooldown <= 0) then { _cooldown = 900; };
|
||||
_cooldown
|
||||
}],
|
||||
["pruneRecentLocations", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
||||
private _now = serverTime;
|
||||
|
||||
_recentLocationRegistry = _recentLocationRegistry select {
|
||||
private _usedAt = _x param [1, -1, [0]];
|
||||
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
|
||||
};
|
||||
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
_recentLocationRegistry
|
||||
}],
|
||||
["getActiveMissionPositions", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _positions = [];
|
||||
{
|
||||
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "destroy") then { continue; };
|
||||
|
||||
private _position = _y getOrDefault ["position", []];
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
_positions pushBack _position;
|
||||
};
|
||||
} forEach _activeMissionRegistry;
|
||||
_positions
|
||||
}],
|
||||
["selectLocation", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
private _taskPos = [];
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 50;
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
|
||||
private _isTooClose = false;
|
||||
{
|
||||
private _prevPos = _x param [0, [], [[]]];
|
||||
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _recentLocationRegistry;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
{
|
||||
if (_candidate distance2D _x < 500) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _activeMissionPositions;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
private _inBlkList = false;
|
||||
{
|
||||
if (_candidate inArea _x) exitWith {
|
||||
_inBlkList = true;
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_taskPos isEqualTo []) exitWith {
|
||||
["WARNING", "Destroy mission generator: selectLocation failed to find a valid dynamic position."] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["position", _taskPos],
|
||||
["grid", mapGridPosition _taskPos]
|
||||
]
|
||||
}],
|
||||
|
||||
["spawnPatrolGroup", compileFinal {
|
||||
params [
|
||||
["_position", [0, 0, 0], [[]]],
|
||||
["_behavior", "patrol", [""]]
|
||||
];
|
||||
|
||||
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
|
||||
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
|
||||
private _groups = [];
|
||||
{
|
||||
if ("attack" in getArray (_x >> "suitable")) then {
|
||||
_groups pushBack _x;
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _sideText = str _side;
|
||||
private _group = createGroup _side;
|
||||
[] call FUNC(updateEnemyCountFromActivePlayers);
|
||||
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
|
||||
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
|
||||
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
|
||||
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
|
||||
|
||||
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
|
||||
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
|
||||
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
|
||||
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
|
||||
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
|
||||
|
||||
if (_minUnits <= 0) then { _minUnits = 1; };
|
||||
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
||||
|
||||
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
|
||||
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
|
||||
{
|
||||
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
|
||||
|
||||
{
|
||||
_unitPool pushBack createHashMapFromArray [
|
||||
["vehicle", getText (_x >> "vehicle")],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_x >> "Units"));
|
||||
} forEach _groups;
|
||||
};
|
||||
|
||||
if (_unitPool isEqualTo []) exitWith {
|
||||
["WARNING", format ["Destroy mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call EFUNC(common,log);
|
||||
deleteGroup _group;
|
||||
grpNull
|
||||
};
|
||||
|
||||
private _leaderPool = _unitPool select {
|
||||
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
|
||||
};
|
||||
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
|
||||
|
||||
private _spawnDefs = [selectRandom _leaderPool];
|
||||
for "_i" from 1 to (_targetUnitCount - 1) do {
|
||||
_spawnDefs pushBack (selectRandom _unitPool);
|
||||
};
|
||||
|
||||
{
|
||||
private _unitClass = _x getOrDefault ["vehicle", ""];
|
||||
if (_unitClass isEqualTo "") then { continue; };
|
||||
|
||||
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
|
||||
if (count _unitOffset < 3) then { _unitOffset resize 3; };
|
||||
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
|
||||
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
|
||||
|
||||
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
|
||||
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
|
||||
} forEach _spawnDefs;
|
||||
|
||||
if (_behavior isEqualTo "defend") then {
|
||||
[_group, _position] call BFUNC(taskDefend);
|
||||
} else {
|
||||
[_group, _position, _patrolRadius] call BFUNC(taskPatrol);
|
||||
};
|
||||
|
||||
["INFO", format [
|
||||
"Destroy mission generator: spawned %1 group. Side=%2, Units=%3, PatrolRadius=%4, Position=%5",
|
||||
_behavior,
|
||||
_side,
|
||||
count (units _group),
|
||||
_patrolRadius,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
_group
|
||||
}],
|
||||
|
||||
["spawnDestroyTargets", compileFinal {
|
||||
params [['_position', [0, 0, 0], [[]]]];
|
||||
|
||||
private _destroyConfig = _self getOrDefault ["destroyConfig", configNull];
|
||||
private _targetClasses = getArray (_destroyConfig >> "Bomb" >> "building");
|
||||
if (_targetClasses isEqualTo []) exitWith { [] };
|
||||
|
||||
private _targetClass = selectRandom _targetClasses;
|
||||
private _nearTargets = nearestObjects [_position, _targetClasses, 250] select {
|
||||
!isNull _x && { alive _x } && { !(_x getVariable ["forge_destroy_reserved", false]) }
|
||||
};
|
||||
|
||||
private _targetObject = objNull;
|
||||
if (_nearTargets isNotEqualTo []) then {
|
||||
_targetObject = selectRandom _nearTargets;
|
||||
_targetObject setVariable ["forge_destroy_reserved", true, true];
|
||||
} else {
|
||||
private _spawnPos = _position vectorAdd [(random 60 - 30), (random 60 - 30), 0];
|
||||
_targetObject = createVehicle [_targetClass, _spawnPos, [], 0, "NONE"];
|
||||
};
|
||||
|
||||
if (isNull _targetObject) exitWith { [] };
|
||||
|
||||
[_targetObject]
|
||||
}],
|
||||
|
||||
["rollRewards", compileFinal {
|
||||
private _destroyConfig = _self getOrDefault ["destroyConfig", configNull];
|
||||
private _equipmentRewards = [];
|
||||
private _supplyRewards = [];
|
||||
private _weaponRewards = [];
|
||||
private _vehicleRewards = [];
|
||||
private _specialRewards = [];
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
{
|
||||
_x params ["_item", "_chance"];
|
||||
if (random 1 < _chance) then {
|
||||
switch (_category) do {
|
||||
case "equipment": { _equipmentRewards pushBack _item; };
|
||||
case "supplies": { _supplyRewards pushBack _item; };
|
||||
case "weapons": { _weaponRewards pushBack _item; };
|
||||
case "vehicles": { _vehicleRewards pushBack _item; };
|
||||
case "special": { _specialRewards pushBack _item; };
|
||||
};
|
||||
};
|
||||
} forEach (getArray (_destroyConfig >> "Rewards" >> _category));
|
||||
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
||||
|
||||
createHashMapFromArray [
|
||||
["equipment", _equipmentRewards],
|
||||
["supplies", _supplyRewards],
|
||||
["weapons", _weaponRewards],
|
||||
["vehicles", _vehicleRewards],
|
||||
["special", _specialRewards]
|
||||
]
|
||||
}],
|
||||
|
||||
["startMission", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _destroyConfig = _self getOrDefault ["destroyConfig", configNull];
|
||||
private _locationData = _self call ["selectLocation", [_manager]];
|
||||
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
||||
|
||||
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
||||
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
|
||||
|
||||
["INFO", format [
|
||||
"Destroy mission generator: selected location. Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
private _group = _self call ["spawnPatrolGroup", [_position]];
|
||||
if (isNull _group) exitWith {
|
||||
["WARNING", format [
|
||||
"Destroy mission generator: spawnPatrolGroup failed for Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
""
|
||||
};
|
||||
|
||||
private _units = units _group;
|
||||
if (_units isEqualTo []) exitWith {
|
||||
["WARNING", format [
|
||||
"Destroy mission generator: spawned group has no units. Grid=%1, Group=%2",
|
||||
_grid,
|
||||
_group
|
||||
]] call EFUNC(common,log);
|
||||
deleteGroup _group;
|
||||
""
|
||||
};
|
||||
|
||||
private _defendGroup = _self call ["spawnPatrolGroup", [_position, "defend"]];
|
||||
if (isNull _defendGroup || { units _defendGroup isEqualTo [] }) then {
|
||||
["WARNING", format [
|
||||
"Destroy mission generator: defensive task group failed for Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
if !(isNull _defendGroup) then {
|
||||
deleteGroup _defendGroup;
|
||||
};
|
||||
};
|
||||
|
||||
private _spawnedGroups = [_group];
|
||||
if !(isNull _defendGroup && { units _defendGroup isNotEqualTo [] }) then {
|
||||
_spawnedGroups pushBack _defendGroup;
|
||||
};
|
||||
|
||||
private _taskID = format ["task_destroy_%1", round (diag_tickTime * 1000)];
|
||||
private _rewardRange = [_destroyConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [10000, 30000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_destroyConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_destroyConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call FUNC(getMissionSettingRange);
|
||||
private _timeRange = [_destroyConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
private _destroyTargets = _self call ["spawnDestroyTargets", [_position]];
|
||||
if (_destroyTargets isEqualTo []) exitWith {
|
||||
{
|
||||
{ deleteVehicle _x; } forEach (units _x);
|
||||
deleteGroup _x;
|
||||
} forEach _spawnedGroups;
|
||||
""
|
||||
};
|
||||
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
|
||||
private _timeLimit = _timeRange call BFUNC(randomNum);
|
||||
private _targetCount = count _destroyTargets;
|
||||
|
||||
private _success = [
|
||||
"destroy",
|
||||
_taskID,
|
||||
_position,
|
||||
format ["Destroy: Grid %1", _grid],
|
||||
format ["Destroy hostile assets operating near grid %1.", _grid],
|
||||
createHashMapFromArray [["targets", _destroyTargets]],
|
||||
createHashMapFromArray [
|
||||
["limitFail", 0],
|
||||
["limitSuccess", _targetCount],
|
||||
["funds", _fundsReward],
|
||||
["ratingFail", _reputationPenalty],
|
||||
["ratingSuccess", _reputationReward],
|
||||
["endSuccess", false],
|
||||
["endFail", false],
|
||||
["timeLimit", _timeLimit],
|
||||
["equipment", _rewards get "equipment"],
|
||||
["supplies", _rewards get "supplies"],
|
||||
["weapons", _rewards get "weapons"],
|
||||
["vehicles", _rewards get "vehicles"],
|
||||
["special", _rewards get "special"]
|
||||
],
|
||||
0,
|
||||
"",
|
||||
"mission_manager"
|
||||
] call FUNC(startTask);
|
||||
|
||||
if !(_success) exitWith {
|
||||
{
|
||||
{ deleteVehicle _x; } forEach (units _x);
|
||||
deleteGroup _x;
|
||||
} forEach _spawnedGroups;
|
||||
""
|
||||
};
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _position],
|
||||
["groups", _spawnedGroups],
|
||||
["startedAt", serverTime]
|
||||
]];
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
_taskID
|
||||
}],
|
||||
|
||||
["completeMission", compileFinal {
|
||||
params [
|
||||
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
|
||||
["_taskID", "", [""]]
|
||||
];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
||||
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
|
||||
|
||||
private _position = _missionRecord getOrDefault ["position", []];
|
||||
private _groups = _missionRecord getOrDefault ["groups", []];
|
||||
{
|
||||
if !(isNull _x) then {
|
||||
{ deleteVehicle _x; } forEach (units _x);
|
||||
deleteGroup _x;
|
||||
};
|
||||
} forEach _groups;
|
||||
|
||||
_activeMissionRegistry deleteAt _taskID;
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
_recentLocationRegistry pushBack [_position, serverTime];
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
@ -0,0 +1,653 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the Hostage mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* N/A. Defines GVAR(HostageMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(HostageMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "HostageMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
|
||||
_self set ["hostageConfig", (_missionConfig >> "MissionTypes" >> "Hostage")];
|
||||
_self set ["generatorType", "hostage"];
|
||||
}],
|
||||
["getGeneratorType", compileFinal {
|
||||
_self getOrDefault ["generatorType", "hostage"]
|
||||
}],
|
||||
["getMissionInterval", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _interval = getNumber (_missionConfig >> "missionInterval");
|
||||
if (_interval <= 0) then { _interval = 300; };
|
||||
_interval
|
||||
}],
|
||||
["getMaxConcurrentMissions", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
|
||||
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
||||
_maxConcurrent
|
||||
}],
|
||||
["getLocationReuseCooldown", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
||||
if (_cooldown <= 0) then { _cooldown = 900; };
|
||||
_cooldown
|
||||
}],
|
||||
["pruneRecentLocations", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
||||
private _now = serverTime;
|
||||
|
||||
_recentLocationRegistry = _recentLocationRegistry select {
|
||||
private _usedAt = _x param [1, -1, [0]];
|
||||
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
|
||||
};
|
||||
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
_recentLocationRegistry
|
||||
}],
|
||||
["getActiveMissionPositions", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _positions = [];
|
||||
{
|
||||
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hostage") then { continue; };
|
||||
|
||||
private _position = _y getOrDefault ["position", []];
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
_positions pushBack _position;
|
||||
};
|
||||
} forEach _activeMissionRegistry;
|
||||
_positions
|
||||
}],
|
||||
["selectLocation", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
private _taskPos = [];
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 50;
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
|
||||
private _isTooClose = false;
|
||||
{
|
||||
private _prevPos = _x param [0, [], [[]]];
|
||||
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _recentLocationRegistry;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
{
|
||||
if (_candidate distance2D _x < 500) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _activeMissionPositions;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
private _inBlkList = false;
|
||||
{
|
||||
if (_candidate inArea _x) exitWith {
|
||||
_inBlkList = true;
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_taskPos isEqualTo []) exitWith {
|
||||
["WARNING", "Hostage mission generator: selectLocation failed to find a valid dynamic position."] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
// Try to bias hostage/shooter spawns to buildings.
|
||||
// We pick a nearby house-like building and later use building positions for spawn points.
|
||||
private _building = objNull;
|
||||
private _buildingCandidates = nearestObjects [
|
||||
_taskPos,
|
||||
["House_F","House","Building","BuildingBase"],
|
||||
200
|
||||
];
|
||||
if (_buildingCandidates isNotEqualTo []) then {
|
||||
_building = selectRandom _buildingCandidates;
|
||||
};
|
||||
|
||||
private _buildingPositions = [];
|
||||
if !(isNull _building) then {
|
||||
// buildingPos returns positions for building interiors; we random-pick from these.
|
||||
for "_i" from 0 to 100 do {
|
||||
private _bp = _building buildingPos _i;
|
||||
if (_bp isEqualTo [0,0,0]) exitWith {};
|
||||
_buildingPositions pushBack _bp;
|
||||
};
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["position", _taskPos],
|
||||
["grid", mapGridPosition _taskPos],
|
||||
["building", _building],
|
||||
["buildingPositions", _buildingPositions]
|
||||
]
|
||||
}],
|
||||
|
||||
["spawnPatrolGroup", compileFinal {
|
||||
params [["_position", [0, 0, 0], [[]]]];
|
||||
|
||||
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
|
||||
private _attackConfig = _self getOrDefault ["attackConfig", configNull];
|
||||
private _groups = [];
|
||||
{
|
||||
if ("attack" in getArray (_x >> "suitable")) then {
|
||||
_groups pushBack _x;
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _sideText = str _side;
|
||||
private _group = createGroup _side;
|
||||
[] call FUNC(updateEnemyCountFromActivePlayers);
|
||||
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
|
||||
private _minUnitsBase = getNumber (_attackConfig >> "minUnits");
|
||||
private _maxUnitsBase = getNumber (_attackConfig >> "maxUnits");
|
||||
private _patrolRadius = getNumber (_attackConfig >> "patrolRadius");
|
||||
|
||||
if (_minUnitsBase <= 0) then { _minUnitsBase = 4; };
|
||||
if (_maxUnitsBase < _minUnitsBase) then { _maxUnitsBase = _minUnitsBase; };
|
||||
if (_patrolRadius <= 0) then { _patrolRadius = 200; };
|
||||
private _minUnits = floor ((_minUnitsBase max 1) * _enemyMult);
|
||||
private _maxUnits = ceil ((_maxUnitsBase max _minUnitsBase) * _enemyMult);
|
||||
|
||||
if (_minUnits <= 0) then { _minUnits = 1; };
|
||||
if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
|
||||
|
||||
private _targetUnitCount = floor random [_minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1];
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
|
||||
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
|
||||
{
|
||||
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
|
||||
|
||||
{
|
||||
_unitPool pushBack createHashMapFromArray [
|
||||
["vehicle", getText (_x >> "vehicle")],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_x >> "Units"));
|
||||
} forEach _groups;
|
||||
};
|
||||
|
||||
if (_unitPool isEqualTo []) exitWith {
|
||||
["WARNING", format ["Hostage mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call EFUNC(common,log);
|
||||
deleteGroup _group;
|
||||
grpNull
|
||||
};
|
||||
|
||||
private _leaderPool = _unitPool select {
|
||||
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
|
||||
};
|
||||
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
|
||||
|
||||
private _spawnDefs = [selectRandom _leaderPool];
|
||||
for "_i" from 1 to (_targetUnitCount - 1) do {
|
||||
_spawnDefs pushBack (selectRandom _unitPool);
|
||||
};
|
||||
|
||||
{
|
||||
private _unitClass = _x getOrDefault ["vehicle", ""];
|
||||
if (_unitClass isEqualTo "") then { continue; };
|
||||
|
||||
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
|
||||
if (count _unitOffset < 3) then { _unitOffset resize 3; };
|
||||
_unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
|
||||
_unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
|
||||
|
||||
private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
|
||||
_unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
|
||||
} forEach _spawnDefs;
|
||||
|
||||
[_group, _position, _patrolRadius] call BFUNC(taskPatrol);
|
||||
|
||||
["INFO", format [
|
||||
"Hostage mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4",
|
||||
_side,
|
||||
count (units _group),
|
||||
_patrolRadius,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
_group
|
||||
}],
|
||||
|
||||
["spawnHostageUnits", compileFinal {
|
||||
params [['_position', [0, 0, 0], [[]]], ['_buildingPositions', []]];
|
||||
|
||||
private _hostageConfig = _self getOrDefault ["hostageConfig", configNull];
|
||||
private _hostageClasses = getArray (_hostageConfig >> "Hostages" >> "civilian") + getArray (_hostageConfig >> "Hostages" >> "military");
|
||||
if (_hostageClasses isEqualTo []) exitWith { [] };
|
||||
|
||||
// Prefer interior building positions when available.
|
||||
private _spawnBasePos = _position;
|
||||
private _useBuildingPositions = (_buildingPositions isEqualTo []);
|
||||
if (_buildingPositions isNotEqualTo []) then {
|
||||
_useBuildingPositions = false;
|
||||
};
|
||||
|
||||
private _hostageCount = 1 + floor (random 2);
|
||||
private _hostageGroup = createGroup civilian;
|
||||
private _hostages = [];
|
||||
for "_i" from 1 to _hostageCount do {
|
||||
private _hostageClass = selectRandom _hostageClasses;
|
||||
|
||||
private _hostagePos = [0,0,0];
|
||||
if !(_useBuildingPositions) then {
|
||||
private _bp = selectRandom _buildingPositions;
|
||||
_hostagePos = _bp;
|
||||
} else {
|
||||
_hostagePos = _spawnBasePos vectorAdd [(random 40 - 20), (random 40 - 20), 0];
|
||||
};
|
||||
|
||||
private _hostage = _hostageGroup createUnit [_hostageClass, _hostagePos, [], 0, "NONE"];
|
||||
if !(isNull _hostage) then {
|
||||
_hostage setCaptive true;
|
||||
_hostages pushBack _hostage;
|
||||
};
|
||||
};
|
||||
|
||||
_hostages
|
||||
}],
|
||||
|
||||
["spawnHostageShooters", compileFinal {
|
||||
params [['_position', [0, 0, 0], [[]]], ['_buildingPositions', []]];
|
||||
|
||||
private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
|
||||
private _groups = [];
|
||||
|
||||
{
|
||||
if ("hostage" in getArray (_x >> "suitable")) then {
|
||||
_groups pushBack _x;
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
|
||||
if (_groups isEqualTo []) then {
|
||||
{
|
||||
if ("attack" in getArray (_x >> "suitable")) then {
|
||||
_groups pushBack _x;
|
||||
};
|
||||
} forEach ("true" configClasses _aiGroupsConfig);
|
||||
};
|
||||
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _sideText = str _side;
|
||||
private _group = createGroup _side;
|
||||
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
|
||||
if (_unitPool isEqualTo [] && { _groups isNotEqualTo [] }) then {
|
||||
{
|
||||
if ((getText (_x >> "side")) isNotEqualTo _sideText) then { continue; };
|
||||
|
||||
{
|
||||
_unitPool pushBack createHashMapFromArray [
|
||||
["vehicle", getText (_x >> "vehicle")],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_x >> "Units"));
|
||||
} forEach _groups;
|
||||
};
|
||||
|
||||
if (_unitPool isEqualTo []) exitWith {
|
||||
deleteGroup _group;
|
||||
[]
|
||||
};
|
||||
|
||||
private _shooterCount = 1 + floor (random 3);
|
||||
private _shooterDefs = [];
|
||||
for "_i" from 1 to _shooterCount do {
|
||||
_shooterDefs pushBack (selectRandom _unitPool);
|
||||
};
|
||||
|
||||
private _shooters = [];
|
||||
// Prefer exterior/adjacent building positions when available.
|
||||
private _shootBasePos = _position;
|
||||
if (_buildingPositions isNotEqualTo []) then {
|
||||
_shootBasePos = selectRandom _buildingPositions;
|
||||
};
|
||||
|
||||
{
|
||||
private _unitClass = _x getOrDefault ["vehicle", ""];
|
||||
|
||||
if (_unitClass isEqualTo "") exitWith { };
|
||||
|
||||
private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
|
||||
if (count _unitOffset < 3) then { _unitOffset resize 3; };
|
||||
_unitOffset set [0, (_unitOffset # 0) + (random 10 - 5)];
|
||||
_unitOffset set [1, (_unitOffset # 1) + (random 10 - 5)];
|
||||
|
||||
private _shooter = _group createUnit [_unitClass, _shootBasePos vectorAdd _unitOffset, [], 0, "NONE"];
|
||||
if !(isNull _shooter) then {
|
||||
_shooter setRank (_x getOrDefault ["rank", "PRIVATE"]);
|
||||
_shooters pushBack _shooter;
|
||||
};
|
||||
} forEach _shooterDefs;
|
||||
|
||||
_shooters
|
||||
}],
|
||||
|
||||
["rollRewards", compileFinal {
|
||||
private _hostageConfig = _self getOrDefault ["hostageConfig", configNull];
|
||||
private _equipmentRewards = [];
|
||||
private _supplyRewards = [];
|
||||
private _weaponRewards = [];
|
||||
private _vehicleRewards = [];
|
||||
private _specialRewards = [];
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
{
|
||||
_x params ["_item", "_chance"];
|
||||
if (random 1 < _chance) then {
|
||||
switch (_category) do {
|
||||
case "equipment": { _equipmentRewards pushBack _item; };
|
||||
case "supplies": { _supplyRewards pushBack _item; };
|
||||
case "weapons": { _weaponRewards pushBack _item; };
|
||||
case "vehicles": { _vehicleRewards pushBack _item; };
|
||||
case "special": { _specialRewards pushBack _item; };
|
||||
};
|
||||
};
|
||||
} forEach (getArray (_hostageConfig >> "Rewards" >> _category));
|
||||
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
||||
|
||||
createHashMapFromArray [
|
||||
["equipment", _equipmentRewards],
|
||||
["supplies", _supplyRewards],
|
||||
["weapons", _weaponRewards],
|
||||
["vehicles", _vehicleRewards],
|
||||
["special", _specialRewards]
|
||||
]
|
||||
}],
|
||||
|
||||
["startMission", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _hostageConfig = _self getOrDefault ["hostageConfig", configNull];
|
||||
private _locationData = _self call ["selectLocation", [_manager]];
|
||||
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
||||
|
||||
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
||||
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
|
||||
private _buildingPositions = _locationData getOrDefault ["buildingPositions", []];
|
||||
|
||||
["INFO", format [
|
||||
"Hostage mission generator: selected location. Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
private _group = _self call ["spawnPatrolGroup", [_position]];
|
||||
if (isNull _group) exitWith {
|
||||
["WARNING", format [
|
||||
"Hostage mission generator: spawnPatrolGroup failed for Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
""
|
||||
};
|
||||
|
||||
private _units = units _group;
|
||||
if (_units isEqualTo []) exitWith {
|
||||
["WARNING", format [
|
||||
"Hostage mission generator: spawned group has no units. Grid=%1, Group=%2",
|
||||
_grid,
|
||||
_group
|
||||
]] call EFUNC(common,log);
|
||||
deleteGroup _group;
|
||||
""
|
||||
};
|
||||
|
||||
private _taskID = format ["task_hostage_%1", round (diag_tickTime * 1000)];
|
||||
private _rewardRange = [_hostageConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [60000, 140000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_hostageConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [12, 25]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_hostageConfig, ["penalty"], "penaltyMin", "penaltyMax", [-16, -6]] call FUNC(getMissionSettingRange);
|
||||
private _timeRange = [_hostageConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
|
||||
private _hostageUnits = _self call ["spawnHostageUnits", [_position, _buildingPositions]];
|
||||
private _shooterUnits = _self call ["spawnHostageShooters", [_position, _buildingPositions]];
|
||||
if (_hostageUnits isEqualTo [] || _shooterUnits isEqualTo []) exitWith { "" };
|
||||
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
|
||||
private _timeLimit = _timeRange call BFUNC(randomNum);
|
||||
|
||||
private _extZone = format ["forge_hostage_ext_zone_%1", _taskID];
|
||||
|
||||
// Choose extraction marker position:
|
||||
// 1) Prefer editor-placed marker containing "ExtZone".
|
||||
// 2) Else, pick a safe point inside a marker containing "blklist".
|
||||
// 3) Else, pick a safe point anywhere on the map at least 2km away from task position.
|
||||
private _extPos = [0, 0, 0];
|
||||
|
||||
private _extZoneMarkers = allMapMarkers select {
|
||||
(toLowerANSI (markerText _x) find "extzone") == 0
|
||||
|| { (toLowerANSI _x find "extzone") == 0 }
|
||||
};
|
||||
|
||||
if (_extZoneMarkers isNotEqualTo []) then {
|
||||
private _mPos = getMarkerPos (selectRandom _extZoneMarkers);
|
||||
// Put marker on ground.
|
||||
private _ground = +_mPos;
|
||||
private _safe = [_ground, 0, 30, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
if (_safe isNotEqualTo [0, 0, 0]) then {
|
||||
_ground = _safe;
|
||||
};
|
||||
_ground set [2, 0];
|
||||
_extPos = _ground;
|
||||
|
||||
} else {
|
||||
// Collect blklist-like markers (rectangle/ellipse) that already exist.
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
if (_blkListMarkers isNotEqualTo []) then {
|
||||
private _selectedBlk = selectRandom _blkListMarkers;
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 60;
|
||||
private _found = false;
|
||||
while { _attempt < _maxAttempts && { !_found } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _markerSize = getMarkerSize _selectedBlk;
|
||||
private _markerRadius = ((_markerSize param [0, 250, [0]]) max (_markerSize param [1, 250, [0]])) max 250;
|
||||
private _candidate = [getMarkerPos _selectedBlk, 0, _markerRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if !(_candidate inArea _selectedBlk) then { continue; };
|
||||
// Ensure it's on land.
|
||||
private _try = +_candidate;
|
||||
_try set [2, 0];
|
||||
_extPos = _try;
|
||||
_found = true;
|
||||
};
|
||||
};
|
||||
|
||||
if (_extPos isEqualTo [0, 0, 0]) then {
|
||||
// Fallback: anywhere on map, at least 2km from task location.
|
||||
private _taskPos2D = +_position;
|
||||
_taskPos2D set [2, 0];
|
||||
|
||||
private _worldMin = 0;
|
||||
private _worldMax = worldSize;
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 80;
|
||||
private _found = false;
|
||||
|
||||
while { _attempt < _maxAttempts && { !_found } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _randX = _worldMin + random (_worldMax - _worldMin);
|
||||
private _randY = _worldMin + random (_worldMax - _worldMin);
|
||||
private _probe = [_randX, _randY, 0];
|
||||
if ((_probe distance2D _taskPos2D) < 2000) then { continue; };
|
||||
private _safe = [_probe, 0, 500, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
if (_safe isEqualTo [0, 0, 0]) then { continue; };
|
||||
if ((_safe distance2D _taskPos2D) < 2000) then { continue; };
|
||||
_safe set [2, 0];
|
||||
_extPos = _safe;
|
||||
_found = true;
|
||||
};
|
||||
|
||||
// Absolute last resort.
|
||||
if (_extPos isEqualTo [0, 0, 0]) then {
|
||||
private _fallback = _position vectorAdd [2500, 0, 0];
|
||||
_fallback set [2, 0];
|
||||
_extPos = _fallback;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
createMarker [_extZone, _extPos];
|
||||
_extZone setMarkerShapeLocal "ELLIPSE";
|
||||
_extZone setMarkerSizeLocal [25, 25];
|
||||
_extZone setMarkerTextLocal format ["Hostage Extraction %1", _grid];
|
||||
_extZone setMarkerAlphaLocal 0.5;
|
||||
_extZone setMarkerBrushLocal "DiagGrid";
|
||||
_extZone setMarkerColor "ColorOrange";
|
||||
|
||||
private _hostageCount = count _hostageUnits;
|
||||
private _limitFail = 1;
|
||||
|
||||
private _success = [
|
||||
"hostage",
|
||||
_taskID,
|
||||
_position,
|
||||
format ["Hostage: Grid %1", _grid],
|
||||
format ["Rescue hostages operating near grid %1.", _grid],
|
||||
createHashMapFromArray [["hostages", _hostageUnits], ["shooters", _shooterUnits]],
|
||||
createHashMapFromArray [
|
||||
["limitFail", _limitFail],
|
||||
["limitSuccess", _hostageCount],
|
||||
["extractionZone", _extZone],
|
||||
["funds", _fundsReward],
|
||||
["ratingFail", _reputationPenalty],
|
||||
["ratingSuccess", _reputationReward],
|
||||
["endSuccess", false],
|
||||
["endFail", false],
|
||||
["timeLimit", _timeLimit],
|
||||
["equipment", _rewards get "equipment"],
|
||||
["supplies", _rewards get "supplies"],
|
||||
["weapons", _rewards get "weapons"],
|
||||
["vehicles", _rewards get "vehicles"],
|
||||
["special", _rewards get "special"],
|
||||
["execution", true],
|
||||
["cbrn", false]
|
||||
],
|
||||
0,
|
||||
"",
|
||||
"mission_manager"
|
||||
] call FUNC(startTask);
|
||||
|
||||
if !(_success) exitWith {
|
||||
deleteMarker _extZone;
|
||||
""
|
||||
};
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _position],
|
||||
["markers", [_extZone]],
|
||||
["startedAt", serverTime]
|
||||
]];
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
_taskID
|
||||
}],
|
||||
|
||||
["completeMission", compileFinal {
|
||||
params [
|
||||
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
|
||||
["_taskID", "", [""]]
|
||||
];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
||||
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
|
||||
|
||||
private _position = _missionRecord getOrDefault ["position", []];
|
||||
private _markers = _missionRecord getOrDefault ["markers", []];
|
||||
{
|
||||
if (_x isEqualType "" && { _x in allMapMarkers }) then {
|
||||
deleteMarker _x;
|
||||
};
|
||||
} forEach _markers;
|
||||
|
||||
_activeMissionRegistry deleteAt _taskID;
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
_recentLocationRegistry pushBack [_position, serverTime];
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
@ -0,0 +1,377 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Defines the HVT kill mission generator base class used by the dynamic
|
||||
* mission manager. The generator selects a location, spawns required
|
||||
* entities, registers a Forge task, and cleans up manager state when the
|
||||
* task completes.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* N/A. Defines GVAR(KillHvtMissionGeneratorBaseClass) in missionNamespace.
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(KillHvtMissionGeneratorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "KillHvtMissionGeneratorBaseClass"],
|
||||
["#create", compileFinal {
|
||||
private _missionConfig = missionConfigFile >> "CfgMissions";
|
||||
if !(isClass _missionConfig) then {
|
||||
_missionConfig = configFile >> "CfgMissions";
|
||||
};
|
||||
_self set ["missionConfig", _missionConfig];
|
||||
_self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
|
||||
_self set ["hvtConfig", (_missionConfig >> "MissionTypes" >> "HVTKill")];
|
||||
_self set ["generatorType", "hvtkill"];
|
||||
}],
|
||||
["getGeneratorType", compileFinal {
|
||||
_self getOrDefault ["generatorType", "hvtkill"]
|
||||
}],
|
||||
["getMissionInterval", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _interval = getNumber (_missionConfig >> "missionInterval");
|
||||
if (_interval <= 0) then { _interval = 300; };
|
||||
_interval
|
||||
}],
|
||||
["getMaxConcurrentMissions", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _maxConcurrent = getNumber (_missionConfig >> "maxConcurrentMissions");
|
||||
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
|
||||
_maxConcurrent
|
||||
}],
|
||||
["getLocationReuseCooldown", compileFinal {
|
||||
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
|
||||
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
|
||||
if (_cooldown <= 0) then { _cooldown = 900; };
|
||||
_cooldown
|
||||
}],
|
||||
["pruneRecentLocations", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
|
||||
private _now = serverTime;
|
||||
|
||||
_recentLocationRegistry = _recentLocationRegistry select {
|
||||
private _usedAt = _x param [1, -1, [0]];
|
||||
(_usedAt >= 0) && { (_now - _usedAt) < _reuseCooldown }
|
||||
};
|
||||
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
_recentLocationRegistry
|
||||
}],
|
||||
["getActiveMissionPositions", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _positions = [];
|
||||
{
|
||||
if ((_y getOrDefault ["generatorType", ""]) isNotEqualTo "hvtkill") then { continue; };
|
||||
|
||||
private _position = _y getOrDefault ["position", []];
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
_positions pushBack _position;
|
||||
};
|
||||
} forEach _activeMissionRegistry;
|
||||
_positions
|
||||
}],
|
||||
["selectLocation", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _worldSize = worldSize;
|
||||
private _center = [_worldSize / 2, _worldSize / 2, 0];
|
||||
private _safeDist = 800;
|
||||
private _playerPos = _center;
|
||||
private _minEdgeDist = _safeDist + 200;
|
||||
private _searchRadius = (_worldSize / 2 - _minEdgeDist) max 500;
|
||||
|
||||
private _recentLocationRegistry = _self call ["pruneRecentLocations", [_manager]];
|
||||
private _activeMissionPositions = _self call ["getActiveMissionPositions", [_manager]];
|
||||
|
||||
private _blkListMarkers = allMapMarkers select { markerShape _x in ["RECTANGLE", "ELLIPSE"] };
|
||||
_blkListMarkers = _blkListMarkers select {
|
||||
(
|
||||
(toLowerANSI _x find "blklist") == 0
|
||||
|| { (toLowerANSI (markerText _x) find "blklist") == 0 }
|
||||
)
|
||||
&& { getMarkerPos _x distance2D [0, 0] > 0 }
|
||||
};
|
||||
|
||||
private _taskPos = [];
|
||||
private _attempt = 0;
|
||||
private _maxAttempts = 50;
|
||||
|
||||
while { _attempt < _maxAttempts && { _taskPos isEqualTo [] } } do {
|
||||
_attempt = _attempt + 1;
|
||||
private _candidate = [_center, _searchRadius, _searchRadius, 3, 0, 0.3, 0] call BFUNC(findSafePos);
|
||||
|
||||
if (_candidate isEqualTo [0, 0, 0]) then { continue; };
|
||||
if (_candidate distance2D _playerPos < _safeDist) then { continue; };
|
||||
|
||||
private _isTooClose = false;
|
||||
{
|
||||
private _prevPos = _x param [0, [], [[]]];
|
||||
if (_prevPos isEqualType [] && { count _prevPos >= 2 } && { _candidate distance2D _prevPos < 500 }) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _recentLocationRegistry;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
{
|
||||
if (_candidate distance2D _x < 500) exitWith {
|
||||
_isTooClose = true;
|
||||
};
|
||||
} forEach _activeMissionPositions;
|
||||
|
||||
if (_isTooClose) then { continue; };
|
||||
|
||||
private _inBlkList = false;
|
||||
{
|
||||
if (_candidate inArea _x) exitWith {
|
||||
_inBlkList = true;
|
||||
};
|
||||
} forEach _blkListMarkers;
|
||||
|
||||
if !(_inBlkList) then {
|
||||
_taskPos = _candidate;
|
||||
};
|
||||
};
|
||||
|
||||
if (_taskPos isEqualTo []) exitWith {
|
||||
["WARNING", "Kill HVT mission generator: selectLocation failed to find a valid dynamic position."] call EFUNC(common,log);
|
||||
createHashMap
|
||||
};
|
||||
|
||||
private _building = objNull;
|
||||
private _buildingCandidates = nearestObjects [
|
||||
_taskPos,
|
||||
["House_F", "House", "Building", "BuildingBase"],
|
||||
200
|
||||
];
|
||||
if (_buildingCandidates isNotEqualTo []) then {
|
||||
_building = selectRandom _buildingCandidates;
|
||||
};
|
||||
|
||||
private _buildingPositions = [];
|
||||
if !(isNull _building) then {
|
||||
for "_i" from 0 to 100 do {
|
||||
private _buildingPos = _building buildingPos _i;
|
||||
if (_buildingPos isEqualTo [0, 0, 0]) exitWith {};
|
||||
_buildingPositions pushBack _buildingPos;
|
||||
};
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["position", _taskPos],
|
||||
["grid", mapGridPosition _taskPos],
|
||||
["buildingPositions", _buildingPositions]
|
||||
]
|
||||
}],
|
||||
|
||||
["spawnHvtTarget", compileFinal {
|
||||
params [['_position', [0, 0, 0], [[]]], ["_buildingPositions", [], [[]]]];
|
||||
|
||||
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
|
||||
private _side = missionNamespace getVariable ["ENEMY_SIDE", east];
|
||||
private _enemyFaction = missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]];
|
||||
private _unitPool = [_enemyFaction, _side] call FUNC(getEnemyFactionUnitPool);
|
||||
if (_unitPool isEqualTo []) exitWith { [] };
|
||||
|
||||
private _leaderPool = _unitPool select {
|
||||
toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
|
||||
};
|
||||
if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
|
||||
|
||||
private _targetDef = selectRandom _leaderPool;
|
||||
private _targetClass = _targetDef getOrDefault ["vehicle", ""];
|
||||
if (_targetClass isEqualTo "") exitWith { [] };
|
||||
|
||||
private _group = createGroup _side;
|
||||
private _leaderPos = if (_buildingPositions isEqualTo []) then {
|
||||
_position vectorAdd [(random 20 - 10), (random 20 - 10), 0]
|
||||
} else {
|
||||
selectRandom _buildingPositions
|
||||
};
|
||||
private _leader = _group createUnit [_targetClass, _leaderPos, [], 0, "NONE"];
|
||||
if (isNull _leader) exitWith {
|
||||
deleteGroup _group;
|
||||
[]
|
||||
};
|
||||
_leader setRank "LIEUTENANT";
|
||||
|
||||
[] call FUNC(updateEnemyCountFromActivePlayers);
|
||||
private _enemyMult = missionNamespace getVariable ["forge_pmc_enemyCountMultiplier", 1];
|
||||
private _escortCount = getNumber (_hvtConfig >> "escorts");
|
||||
if (_escortCount < 0) then { _escortCount = 0; };
|
||||
_escortCount = floor (_escortCount * _enemyMult);
|
||||
private _escortUnits = [];
|
||||
for "_i" from 1 to _escortCount do {
|
||||
private _escortDef = selectRandom _unitPool;
|
||||
private _escortClass = _escortDef getOrDefault ["vehicle", ""];
|
||||
if (_escortClass isEqualTo "") then { continue; };
|
||||
private _escortPos = if (_buildingPositions isEqualTo []) then {
|
||||
_position vectorAdd [(random 35 - 17), (random 35 - 17), 0]
|
||||
} else {
|
||||
selectRandom _buildingPositions
|
||||
};
|
||||
private _escort = _group createUnit [_escortClass, _escortPos, [], 0, "NONE"];
|
||||
if !(isNull _escort) then {
|
||||
_escort setRank (_escortDef getOrDefault ["rank", "PRIVATE"]);
|
||||
_escortUnits pushBack _escort;
|
||||
};
|
||||
};
|
||||
|
||||
private _groupUnits = [_leader] + _escortUnits;
|
||||
|
||||
[_group, _position, 200] call BFUNC(taskPatrol);
|
||||
|
||||
[_leader, _groupUnits]
|
||||
}],
|
||||
|
||||
["rollRewards", compileFinal {
|
||||
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
|
||||
private _equipmentRewards = [];
|
||||
private _supplyRewards = [];
|
||||
private _weaponRewards = [];
|
||||
private _vehicleRewards = [];
|
||||
private _specialRewards = [];
|
||||
|
||||
{
|
||||
private _category = _x;
|
||||
{
|
||||
_x params ["_item", "_chance"];
|
||||
if (random 1 < _chance) then {
|
||||
switch (_category) do {
|
||||
case "equipment": { _equipmentRewards pushBack _item; };
|
||||
case "supplies": { _supplyRewards pushBack _item; };
|
||||
case "weapons": { _weaponRewards pushBack _item; };
|
||||
case "vehicles": { _vehicleRewards pushBack _item; };
|
||||
case "special": { _specialRewards pushBack _item; };
|
||||
};
|
||||
};
|
||||
} forEach (getArray (_hvtConfig >> "Rewards" >> _category));
|
||||
} forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
|
||||
|
||||
createHashMapFromArray [
|
||||
["equipment", _equipmentRewards],
|
||||
["supplies", _supplyRewards],
|
||||
["weapons", _weaponRewards],
|
||||
["vehicles", _vehicleRewards],
|
||||
["special", _specialRewards]
|
||||
]
|
||||
}],
|
||||
|
||||
["startMission", compileFinal {
|
||||
params [["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]]];
|
||||
|
||||
private _hvtConfig = _self getOrDefault ["hvtConfig", configNull];
|
||||
private _locationData = _self call ["selectLocation", [_manager]];
|
||||
if (_locationData isEqualTo createHashMap) exitWith { "" };
|
||||
|
||||
private _position = _locationData getOrDefault ["position", [0, 0, 0]];
|
||||
private _grid = _locationData getOrDefault ["grid", mapGridPosition _position];
|
||||
private _buildingPositions = _locationData getOrDefault ["buildingPositions", []];
|
||||
|
||||
["INFO", format [
|
||||
"Kill HVT mission generator: selected location. Grid=%1, Position=%2",
|
||||
_grid,
|
||||
_position
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
private _taskID = format ["task_kill_hvt_%1", round (diag_tickTime * 1000)];
|
||||
private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call FUNC(getMissionSettingRange);
|
||||
private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call FUNC(getMissionSettingRange);
|
||||
private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call FUNC(getMissionSettingRange);
|
||||
private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call FUNC(getMissionSettingRange);
|
||||
private _rewards = _self call ["rollRewards"];
|
||||
|
||||
private _spawnResult = _self call ["spawnHvtTarget", [_position, _buildingPositions]];
|
||||
if !(_spawnResult isEqualType [] && { count _spawnResult >= 2 }) exitWith { "" };
|
||||
private _hvtTarget = _spawnResult select 0;
|
||||
private _hvtGroupUnits = _spawnResult select 1;
|
||||
if (isNull _hvtTarget || _hvtGroupUnits isEqualTo []) exitWith { "" };
|
||||
|
||||
private _fundsReward = _rewardRange call BFUNC(randomNum);
|
||||
private _reputationReward = _reputationRange call BFUNC(randomNum);
|
||||
private _reputationPenalty = _penaltyRange call BFUNC(randomNum);
|
||||
private _timeLimit = _timeRange call BFUNC(randomNum);
|
||||
|
||||
private _success = [
|
||||
"hvt",
|
||||
_taskID,
|
||||
_position,
|
||||
format ["HVT: Grid %1", _grid],
|
||||
format ["Eliminate a high-value target near grid %1.", _grid],
|
||||
createHashMapFromArray [["hvts", [_hvtTarget]]],
|
||||
createHashMapFromArray [
|
||||
["limitFail", 0],
|
||||
["limitSuccess", 1],
|
||||
["captureHvt", false],
|
||||
["funds", _fundsReward],
|
||||
["ratingFail", _reputationPenalty],
|
||||
["ratingSuccess", _reputationReward],
|
||||
["endSuccess", false],
|
||||
["endFail", false],
|
||||
["timeLimit", _timeLimit],
|
||||
["equipment", _rewards get "equipment"],
|
||||
["supplies", _rewards get "supplies"],
|
||||
["weapons", _rewards get "weapons"],
|
||||
["vehicles", _rewards get "vehicles"],
|
||||
["special", _rewards get "special"]
|
||||
],
|
||||
0,
|
||||
"",
|
||||
"mission_manager"
|
||||
] call FUNC(startTask);
|
||||
|
||||
if !(_success) exitWith { "" };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
_activeMissionRegistry set [_taskID, createHashMapFromArray [
|
||||
["generatorType", _self call ["getGeneratorType", []]],
|
||||
["position", _position],
|
||||
["startedAt", serverTime]
|
||||
]];
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
_taskID
|
||||
}],
|
||||
|
||||
["completeMission", compileFinal {
|
||||
params [
|
||||
["_manager", createHashMapObject [createHashMapFromArray []], [createHashMap]],
|
||||
["_taskID", "", [""]]
|
||||
];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap];
|
||||
private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
|
||||
if ((_missionRecord getOrDefault ["generatorType", ""]) isNotEqualTo (_self call ["getGeneratorType", []])) exitWith { false };
|
||||
|
||||
private _position = _missionRecord getOrDefault ["position", []];
|
||||
private _markers = _missionRecord getOrDefault ["markers", []];
|
||||
{
|
||||
if (_x isEqualType "" && { _x in allMapMarkers }) then {
|
||||
deleteMarker _x;
|
||||
};
|
||||
} forEach _markers;
|
||||
|
||||
_activeMissionRegistry deleteAt _taskID;
|
||||
_manager set ["activeMissionRegistry", _activeMissionRegistry];
|
||||
|
||||
if (_position isEqualType [] && { count _position >= 2 }) then {
|
||||
private _recentLocationRegistry = _manager getOrDefault ["recentLocationRegistry", []];
|
||||
_recentLocationRegistry pushBack [_position, serverTime];
|
||||
_manager set ["recentLocationRegistry", _recentLocationRegistry];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
@ -0,0 +1,103 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Builds an infantry unit pool for the selected enemy faction. The returned
|
||||
* entries match the generator spawn format.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Faction classname <STRING> (Default: ENEMY_FACTION_STR or "IND_G_F")
|
||||
* 1: Fallback side <SIDE> (Default: ENEMY_SIDE or east)
|
||||
* 2: Allow side-default fallback units when no faction units exist <BOOL>
|
||||
* (Default: true)
|
||||
*
|
||||
* Return Value:
|
||||
* Unit definitions with vehicle, rank, and position keys <ARRAY>
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
params [
|
||||
["_faction", missionNamespace getVariable ["ENEMY_FACTION_STR", "IND_G_F"], [""]],
|
||||
["_fallbackSide", missionNamespace getVariable ["ENEMY_SIDE", east], [east]],
|
||||
["_allowSideFallback", true, [false]]
|
||||
];
|
||||
|
||||
if (_faction isEqualTo "") then {
|
||||
_faction = "IND_G_F";
|
||||
};
|
||||
|
||||
private _pool = [];
|
||||
private _sideNumber = [_fallbackSide] call BIS_fnc_sideID;
|
||||
|
||||
// Check CfgFactionUnitMap first for explicit faction unit definitions
|
||||
private _factionMapRoot = missionConfigFile >> "CfgFactionUnitMap";
|
||||
if !(isClass _factionMapRoot) then {
|
||||
_factionMapRoot = configFile >> "CfgFactionUnitMap";
|
||||
};
|
||||
|
||||
private _factionMapConfig = _factionMapRoot >> _faction;
|
||||
if (isClass _factionMapConfig) then {
|
||||
{
|
||||
private _vehicle = getText (_x >> "vehicle");
|
||||
if (_vehicle isEqualTo "" || { !(isClass (configFile >> "CfgVehicles" >> _vehicle)) }) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
_pool pushBack createHashMapFromArray [
|
||||
["vehicle", _vehicle],
|
||||
["rank", getText (_x >> "rank")],
|
||||
["position", getArray (_x >> "position")]
|
||||
];
|
||||
} forEach ("true" configClasses (_factionMapConfig >> "Units"));
|
||||
};
|
||||
|
||||
// Fall back to config traversal if no explicit mapping exists.
|
||||
if (_pool isEqualTo []) then {
|
||||
private _factionFallback = _faction;
|
||||
|
||||
{
|
||||
if (getNumber (_x >> "scope") < 2) then { continue; };
|
||||
private _unitFaction = getText (_x >> "faction");
|
||||
if ((_unitFaction isNotEqualTo _faction) && (_unitFaction isNotEqualTo _factionFallback)) then { continue; };
|
||||
if (getNumber (_x >> "side") isNotEqualTo _sideNumber) then { continue; };
|
||||
if !(configName _x isKindOf "CAManBase") then { continue; };
|
||||
|
||||
private _className = configName _x;
|
||||
private _upperClassName = toUpperANSI _className;
|
||||
private _rank = "PRIVATE";
|
||||
|
||||
if (
|
||||
(_upperClassName find "_SL_" >= 0)
|
||||
|| { _upperClassName find "_TL_" >= 0 }
|
||||
|| { _upperClassName find "OFFICER" >= 0 }
|
||||
|| { _upperClassName find "COMMANDER" >= 0 }
|
||||
) then {
|
||||
_rank = "SERGEANT";
|
||||
};
|
||||
|
||||
_pool pushBack createHashMapFromArray [
|
||||
["vehicle", _className],
|
||||
["rank", _rank],
|
||||
["position", [0, 0, 0]]
|
||||
];
|
||||
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||
};
|
||||
|
||||
if (_pool isEqualTo [] && { _allowSideFallback }) then {
|
||||
private _fallbackUnits = switch (_fallbackSide) do {
|
||||
case east: { ["O_Soldier_SL_F", "O_Soldier_TL_F", "O_Soldier_F", "O_Soldier_AR_F", "O_Soldier_GL_F", "O_medic_F"] };
|
||||
case resistance: { ["I_G_Soldier_SL_F", "I_G_Soldier_TL_F", "I_G_Soldier_F", "I_G_Soldier_AR_F", "I_G_medic_F"] };
|
||||
default { ["O_Soldier_SL_F", "O_Soldier_TL_F", "O_Soldier_F", "O_Soldier_AR_F", "O_medic_F"] };
|
||||
};
|
||||
|
||||
{
|
||||
_pool pushBack createHashMapFromArray [
|
||||
["vehicle", _x],
|
||||
["rank", ["PRIVATE", "SERGEANT"] select (_forEachIndex == 0)],
|
||||
["position", [0, 0, 0]]
|
||||
];
|
||||
} forEach _fallbackUnits;
|
||||
};
|
||||
|
||||
_pool
|
||||
@ -0,0 +1,54 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Resolves a numeric mission range, preferring startup UI settings when
|
||||
* present and falling back to CfgMissions values.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Root config class <CONFIG>
|
||||
* 1: Config path segments to the range array <ARRAY>
|
||||
* 2: Mission settings min key <STRING>
|
||||
* 3: Mission settings max key <STRING>
|
||||
* 4: Fallback [min, max] range <ARRAY> (Default: [0, 0])
|
||||
*
|
||||
* Return Value:
|
||||
* Numeric [min, max] range <ARRAY>. Reversed input is sorted so reputation-hit
|
||||
* fields can use -5 / -25 semantics while generators still receive [-25, -5].
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
params [
|
||||
["_config", configNull, [configNull]],
|
||||
["_path", [], [[]]],
|
||||
["_minKey", "", [""]],
|
||||
["_maxKey", "", [""]],
|
||||
["_fallback", [0, 0], [[]]]
|
||||
];
|
||||
|
||||
private _rangeConfig = _config;
|
||||
{
|
||||
_rangeConfig = _rangeConfig >> _x;
|
||||
} forEach _path;
|
||||
|
||||
private _range = getArray _rangeConfig;
|
||||
private _fallbackMin = _fallback param [0, 0, [0]];
|
||||
private _fallbackMax = _fallback param [1, _fallbackMin, [0]];
|
||||
|
||||
private _min = _range param [0, _fallbackMin, [0]];
|
||||
private _max = _range param [1, _fallbackMax, [0]];
|
||||
|
||||
private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap];
|
||||
if (_settings isEqualType createHashMap) then {
|
||||
_min = _settings getOrDefault [_minKey, _min];
|
||||
_max = _settings getOrDefault [_maxKey, _max];
|
||||
};
|
||||
|
||||
if (_max < _min) then {
|
||||
private _swap = _min;
|
||||
_min = _max;
|
||||
_max = _swap;
|
||||
};
|
||||
|
||||
[_min, _max]
|
||||
@ -0,0 +1,57 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions, Blackbox AI, MrPākehā
|
||||
* Calculates enemy spawn scaling from active player count and stores the
|
||||
* result in missionNamespace for mission generators.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Enemy count multiplier <NUMBER>
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
if !(isServer) exitWith { 1 };
|
||||
|
||||
private _table = missionNamespace getVariable [
|
||||
"forge_pmc_enemyCountMultiplierTable",
|
||||
[
|
||||
[1, 2, 0.75],
|
||||
[3, 6, 1.0],
|
||||
[7, 10, 1.25],
|
||||
[11, 19, 1.5]
|
||||
]
|
||||
];
|
||||
|
||||
private _minMultiplier = missionNamespace getVariable ["forge_pmc_enemyCountMultiplierMin", 0.5];
|
||||
private _maxMultiplier = missionNamespace getVariable ["forge_pmc_enemyCountMultiplierMax", 2.0];
|
||||
|
||||
private _activeCount = {
|
||||
(isPlayer _x) && { alive _x }
|
||||
} count allPlayers;
|
||||
|
||||
private _activeCountSafe = _activeCount max 1;
|
||||
private _multiplier = 1;
|
||||
|
||||
{
|
||||
_x params ["_min", "_max", "_value"];
|
||||
if (_activeCountSafe >= _min && { _activeCountSafe <= _max }) exitWith {
|
||||
_multiplier = _value;
|
||||
};
|
||||
} forEach _table;
|
||||
|
||||
_multiplier = (_multiplier max _minMultiplier) min _maxMultiplier;
|
||||
|
||||
missionNamespace setVariable ["forge_pmc_activePlayerCount", _activeCountSafe, true];
|
||||
missionNamespace setVariable ["forge_pmc_enemyCountMultiplier", _multiplier, true];
|
||||
|
||||
["INFO", format [
|
||||
"Mission enemy scaling updated. ActivePlayers=%1, Multiplier=%2",
|
||||
_activeCountSafe,
|
||||
_multiplier
|
||||
]] call EFUNC(common,log);
|
||||
|
||||
_multiplier
|
||||
@ -69,6 +69,26 @@ Common generated IDs:
|
||||
| `cad:groups:build` | `groups_seed_json` | Group array JSON. |
|
||||
| `cad:view:hydrate` | `hydrate_seed_json` | Hydrated CAD payload JSON. |
|
||||
|
||||
## Generated Mission Requests
|
||||
|
||||
Dispatchers can request framework-generated mission tasks from the CAD
|
||||
dispatcher board. The server hydrates the available generated task types from
|
||||
the task mission manager as `generatedTaskTypes`; the client uses that hydrated
|
||||
list for the dropdown.
|
||||
|
||||
Generated mission requests are controlled by the server CBA setting
|
||||
`forge_task_enableGenerator`:
|
||||
|
||||
- Enabled: CAD receives the generated task type list and dispatchers can request
|
||||
a specific generator type.
|
||||
- Disabled: CAD receives an empty generated task type list, the task request UI
|
||||
is disabled, and server-side request handling rejects any manual request.
|
||||
|
||||
The framework-owned request entry point is
|
||||
`forge_server_task_fnc_requestMissionTask`. Server CAD calls that first and only
|
||||
falls back to a mission-local `forge_pmc_fnc_requestMissionTask` when the
|
||||
framework entry point is unavailable.
|
||||
|
||||
## Submit a Support Request
|
||||
|
||||
```sqf
|
||||
@ -175,6 +195,7 @@ private _session = createHashMapFromArray [
|
||||
private _seed = createHashMapFromArray [
|
||||
["groups", _liveGroups],
|
||||
["activeTasks", _activeTasks],
|
||||
["generatedTaskTypes", _generatedTaskTypes],
|
||||
["session", _session]
|
||||
];
|
||||
|
||||
|
||||
@ -22,8 +22,8 @@ The native Arma map remains part of the same display.
|
||||
## Repository and Bridge
|
||||
|
||||
`forge_client_cad_fnc_initRepository` caches the hydrated CAD payload,
|
||||
selected mode, dispatch view, session data, groups, tasks, requests, and
|
||||
assignments.
|
||||
selected mode, dispatch view, session data, groups, tasks, requests,
|
||||
assignments, and server-provided generated task types.
|
||||
|
||||
`forge_client_cad_fnc_initUIBridge` owns:
|
||||
|
||||
@ -57,6 +57,7 @@ focus uses the member position included in the hydrated group roster.
|
||||
| `cad::mode::set` | Switch between operations and dispatch mode. |
|
||||
| `cad::dispatchView::set` | Switch dispatch board/map view. |
|
||||
| `cad::refresh` | Request fresh CAD hydrate data. |
|
||||
| `cad::generatedTask::request` | Request a server-generated mission task from the selected generator type. |
|
||||
| `cad::tasks::assign` | Assign a task to a group. |
|
||||
| `cad::tasks::acknowledge` | Acknowledge assigned task. |
|
||||
| `cad::tasks::decline` | Decline assigned task. |
|
||||
@ -99,6 +100,13 @@ normal CAD-compatible task sources because they register task catalog data.
|
||||
Direct handler or task-function calls only work with CAD when the task catalog
|
||||
entry already exists.
|
||||
|
||||
The dispatcher-generated task dropdown is hydrated from the server
|
||||
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
|
||||
older payload compatibility, but any hydrate payload that includes
|
||||
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
|
||||
request control, which is how `forge_task_enableGenerator = false` is surfaced
|
||||
client-side.
|
||||
|
||||
## Authorization Notes
|
||||
|
||||
Only dispatcher sessions can enter dispatch mode. If the hydrated session is
|
||||
|
||||
@ -728,6 +728,21 @@ Validation:
|
||||
|
||||
## Mission Manager Blacklist Markers
|
||||
|
||||
Generated missions are configured through mission-local `CfgMissions.hpp`.
|
||||
Place it in the mission folder and include it from `description.ext` so mission
|
||||
designers own task weights, reward ranges, time limits, HVT/hostage class
|
||||
pools, defuse device pools, delivery cargo, destroy targets, and the
|
||||
`locationReuseCooldown`.
|
||||
|
||||
If a mission does not define `CfgMissions`, the task addon falls back to the
|
||||
framework copy at `arma/server/addons/task/CfgMissions.hpp`. Treat the
|
||||
framework file as the default schema and mission copies as the tuning layer.
|
||||
|
||||
The generated mission system supports `attack`, `defend`, `defuse`,
|
||||
`delivery`, `destroy`, `hostage`, `hvtkill`, and `hvtcapture`. The
|
||||
`forge_task_enableGenerator` CBA setting gates both timer-based generation and
|
||||
CAD dispatcher-requested generation.
|
||||
|
||||
The dynamic mission generator avoids rectangle and ellipse area markers whose
|
||||
marker name or marker text starts with `blklist`.
|
||||
|
||||
|
||||
@ -140,9 +140,47 @@ Mission designers can create tasks in four ways:
|
||||
intentionally fall back to the `default` org. This path expects the BIS task
|
||||
to already exist if map-task visibility is required.
|
||||
|
||||
The dynamic mission manager can also generate attack tasks from config. That is
|
||||
The dynamic mission manager can also generate attack, defend, defuse, delivery,
|
||||
destroy, hostage, HVT kill, and HVT capture tasks from config. That is
|
||||
system-generated content rather than a hand-authored task creation path.
|
||||
|
||||
## Generated Mission Configuration
|
||||
|
||||
Mission designers should define `class CfgMissions` in the mission folder, such
|
||||
as `arma/missions/<missionName>/CfgMissions.hpp`, and include it from
|
||||
`description.ext`. The framework also ships
|
||||
`arma/server/addons/task/CfgMissions.hpp` as a fallback schema for missions that
|
||||
do not provide their own config.
|
||||
|
||||
The generator lookup order is:
|
||||
|
||||
1. `missionConfigFile >> "CfgMissions"`
|
||||
2. `configFile >> "CfgMissions"`
|
||||
|
||||
Mission config therefore wins. Use mission-local `CfgMissions.hpp` for task
|
||||
weights, reward ranges, time limits, object pools, HVT/hostage classes, defuse
|
||||
device pools, delivery cargo pools, destroy targets, and location reuse
|
||||
cooldown. Keep the framework copy in sync with the expected schema so fallback
|
||||
generation still works.
|
||||
|
||||
Generated mission types currently exposed by the framework are:
|
||||
|
||||
| Type | CAD Value | Base Task Flow |
|
||||
| --- | --- | --- |
|
||||
| Attack | `attack` | `attack` |
|
||||
| Defend | `defend` | `defend` |
|
||||
| Defuse | `defuse` | `defuse` |
|
||||
| Delivery | `delivery` | `delivery` |
|
||||
| Destroy | `destroy` | `destroy` |
|
||||
| Hostage | `hostage` | `hostage` |
|
||||
| Kill HVT | `hvtkill` | `hvt` |
|
||||
| Capture HVT | `hvtcapture` | `hvt` |
|
||||
|
||||
The server CBA setting `forge_task_enableGenerator` is the single runtime gate
|
||||
for generated missions. When disabled, timer-based generation does not run, CAD
|
||||
hydrates no generated task types, and manual dispatcher requests are rejected
|
||||
server-side.
|
||||
|
||||
## CAD Compatibility
|
||||
|
||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||
@ -159,7 +197,7 @@ CAD-compatible creation paths:
|
||||
`forge_server_task_fnc_startTask`.
|
||||
- `forge_server_task_fnc_startTask`: compatible because it registers the
|
||||
catalog entry, creates the BIS task, and dispatches through the handler.
|
||||
- Dynamic mission manager attack tasks: compatible because the mission manager
|
||||
- Dynamic mission manager tasks: compatible because the mission manager
|
||||
uses `forge_server_task_fnc_startTask`.
|
||||
|
||||
Limited or incompatible paths:
|
||||
|
||||
@ -150,8 +150,7 @@ Minimum Eden setup:
|
||||
|
||||
Transport nodes are generic paid travel points. They can represent ferries,
|
||||
airports, bus stops, teleport terminals, or any other mission transport system.
|
||||
The framework owns the menu, billing, cargo scan, and movement logic. The
|
||||
mission only needs placed objects and optional arrival markers.
|
||||
The framework owns the menu, billing, cargo scan, and movement logic.
|
||||
|
||||

|
||||
|
||||
@ -171,7 +170,7 @@ transport_2
|
||||
transport_10
|
||||
```
|
||||
|
||||
Place optional arrival markers with matching suffixes:
|
||||
Place arrival markers with matching suffixes:
|
||||
|
||||
```text
|
||||
transport_arrival
|
||||
@ -729,6 +728,21 @@ Validation:
|
||||
|
||||
## Mission Manager Blacklist Markers
|
||||
|
||||
Generated missions are configured through mission-local `CfgMissions.hpp`.
|
||||
Place it in the mission folder and include it from `description.ext` so mission
|
||||
designers own task weights, reward ranges, time limits, HVT/hostage class
|
||||
pools, defuse device pools, delivery cargo, destroy targets, and the
|
||||
`locationReuseCooldown`.
|
||||
|
||||
If a mission does not define `CfgMissions`, the task addon falls back to the
|
||||
framework copy at `arma/server/addons/task/CfgMissions.hpp`. Treat the
|
||||
framework file as the default schema and mission copies as the tuning layer.
|
||||
|
||||
The generated mission system supports `attack`, `defend`, `defuse`,
|
||||
`delivery`, `destroy`, `hostage`, `hvtkill`, and `hvtcapture`. The
|
||||
`forge_task_enableGenerator` CBA setting gates both timer-based generation and
|
||||
CAD dispatcher-requested generation.
|
||||
|
||||
The dynamic mission generator avoids rectangle and ellipse area markers whose
|
||||
marker name or marker text starts with `blklist`.
|
||||
|
||||
|
||||
@ -226,7 +226,7 @@ Player workflow:
|
||||
1. Stand near a transport point.
|
||||
2. Open the actor interaction menu.
|
||||
3. Select Transport.
|
||||
4. Select a destination from the transport submenu, or select Close to return
|
||||
4. Select a destination from the transport submenu, or select Back to return
|
||||
to the default interaction menu.
|
||||
|
||||

|
||||
|
||||
@ -139,9 +139,47 @@ Mission designers can create tasks in four ways:
|
||||
intentionally fall back to the `default` org. This path expects the BIS task
|
||||
to already exist if map-task visibility is required.
|
||||
|
||||
The dynamic mission manager can also generate attack tasks from config. That is
|
||||
The dynamic mission manager can also generate attack, defend, defuse, delivery,
|
||||
destroy, hostage, HVT kill, and HVT capture tasks from config. That is
|
||||
system-generated content rather than a hand-authored task creation path.
|
||||
|
||||
## Generated Mission Configuration
|
||||
|
||||
Mission designers should define `class CfgMissions` in the mission folder, such
|
||||
as `arma/missions/<missionName>/CfgMissions.hpp`, and include it from
|
||||
`description.ext`. The framework also ships
|
||||
`arma/server/addons/task/CfgMissions.hpp` as a fallback schema for missions that
|
||||
do not provide their own config.
|
||||
|
||||
The generator lookup order is:
|
||||
|
||||
1. `missionConfigFile >> "CfgMissions"`
|
||||
2. `configFile >> "CfgMissions"`
|
||||
|
||||
Mission config therefore wins. Use mission-local `CfgMissions.hpp` for task
|
||||
weights, reward ranges, time limits, object pools, HVT/hostage classes, defuse
|
||||
device pools, delivery cargo pools, destroy targets, and location reuse
|
||||
cooldown. Keep the framework copy in sync with the expected schema so fallback
|
||||
generation still works.
|
||||
|
||||
Generated mission types currently exposed by the framework are:
|
||||
|
||||
| Type | CAD Value | Base Task Flow |
|
||||
| --- | --- | --- |
|
||||
| Attack | `attack` | `attack` |
|
||||
| Defend | `defend` | `defend` |
|
||||
| Defuse | `defuse` | `defuse` |
|
||||
| Delivery | `delivery` | `delivery` |
|
||||
| Destroy | `destroy` | `destroy` |
|
||||
| Hostage | `hostage` | `hostage` |
|
||||
| Kill HVT | `hvtkill` | `hvt` |
|
||||
| Capture HVT | `hvtcapture` | `hvt` |
|
||||
|
||||
The server CBA setting `forge_task_enableGenerator` is the single runtime gate
|
||||
for generated missions. When disabled, timer-based generation does not run, CAD
|
||||
hydrates no generated task types, and manual dispatcher requests are rejected
|
||||
server-side.
|
||||
|
||||
## CAD Compatibility
|
||||
|
||||
CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must
|
||||
@ -158,7 +196,7 @@ CAD-compatible creation paths:
|
||||
`forge_server_task_fnc_startTask`.
|
||||
- `forge_server_task_fnc_startTask`: compatible because it registers the
|
||||
catalog entry, creates the BIS task, and dispatches through the handler.
|
||||
- Dynamic mission manager attack tasks: compatible because the mission manager
|
||||
- Dynamic mission manager tasks: compatible because the mission manager
|
||||
uses `forge_server_task_fnc_startTask`.
|
||||
|
||||
Limited or incompatible paths:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Transport Service Guide"
|
||||
description: "The transport service provides paid point-to-point travel for players and nearby vehicles or passengers. It is framework-owned: missions only need placed transport objects and optional arrival markers with the expected variable names."
|
||||
description: "The transport service provides paid point-to-point travel for players and nearby vehicles or passengers. It is framework-owned: missions only need placed transport objects and arrival markers with the expected variable names."
|
||||
---
|
||||
|
||||
## Mission Contract
|
||||
@ -134,9 +134,3 @@ These screenshots show the default transport setup and player workflow:
|
||||

|
||||
|
||||

|
||||
|
||||
## Mission-Side Code Requirement
|
||||
|
||||
No mission-side transport service, addAction script, or server event bridge is
|
||||
required. The framework handles menu discovery, destination selection, pricing,
|
||||
billing, cargo movement, and EventBus notifications.
|
||||
|
||||
@ -67,6 +67,26 @@ Common generated IDs:
|
||||
| `cad:groups:build` | `groups_seed_json` | Group array JSON. |
|
||||
| `cad:view:hydrate` | `hydrate_seed_json` | Hydrated CAD payload JSON. |
|
||||
|
||||
## Generated Mission Requests
|
||||
|
||||
Dispatchers can request framework-generated mission tasks from the CAD
|
||||
dispatcher board. The server hydrates the available generated task types from
|
||||
the task mission manager as `generatedTaskTypes`; the client uses that hydrated
|
||||
list for the dropdown.
|
||||
|
||||
Generated mission requests are controlled by the server CBA setting
|
||||
`forge_task_enableGenerator`:
|
||||
|
||||
- Enabled: CAD receives the generated task type list and dispatchers can request
|
||||
a specific generator type.
|
||||
- Disabled: CAD receives an empty generated task type list, the task request UI
|
||||
is disabled, and server-side request handling rejects any manual request.
|
||||
|
||||
The framework-owned request entry point is
|
||||
`forge_server_task_fnc_requestMissionTask`. Server CAD calls that first and only
|
||||
falls back to a mission-local `forge_pmc_fnc_requestMissionTask` when the
|
||||
framework entry point is unavailable.
|
||||
|
||||
## Submit a Support Request
|
||||
|
||||
```sqf
|
||||
@ -173,6 +193,7 @@ private _session = createHashMapFromArray [
|
||||
private _seed = createHashMapFromArray [
|
||||
["groups", _liveGroups],
|
||||
["activeTasks", _activeTasks],
|
||||
["generatedTaskTypes", _generatedTaskTypes],
|
||||
["session", _session]
|
||||
];
|
||||
|
||||
|
||||
@ -21,8 +21,8 @@ The native Arma map remains part of the same display.
|
||||
## Repository and Bridge
|
||||
|
||||
`forge_client_cad_fnc_initRepository` caches the hydrated CAD payload,
|
||||
selected mode, dispatch view, session data, groups, tasks, requests, and
|
||||
assignments.
|
||||
selected mode, dispatch view, session data, groups, tasks, requests,
|
||||
assignments, and server-provided generated task types.
|
||||
|
||||
`forge_client_cad_fnc_initUIBridge` owns:
|
||||
|
||||
@ -56,6 +56,7 @@ focus uses the member position included in the hydrated group roster.
|
||||
| `cad::mode::set` | Switch between operations and dispatch mode. |
|
||||
| `cad::dispatchView::set` | Switch dispatch board/map view. |
|
||||
| `cad::refresh` | Request fresh CAD hydrate data. |
|
||||
| `cad::generatedTask::request` | Request a server-generated mission task from the selected generator type. |
|
||||
| `cad::tasks::assign` | Assign a task to a group. |
|
||||
| `cad::tasks::acknowledge` | Acknowledge assigned task. |
|
||||
| `cad::tasks::decline` | Decline assigned task. |
|
||||
@ -98,6 +99,13 @@ normal CAD-compatible task sources because they register task catalog data.
|
||||
Direct handler or task-function calls only work with CAD when the task catalog
|
||||
entry already exists.
|
||||
|
||||
The dispatcher-generated task dropdown is hydrated from the server
|
||||
`generatedTaskTypes` payload. The UI has a built-in fallback list for loading or
|
||||
older payload compatibility, but any hydrate payload that includes
|
||||
`generatedTaskTypes` replaces that fallback. An empty hydrated list disables the
|
||||
request control, which is how `forge_task_enableGenerator = false` is surfaced
|
||||
client-side.
|
||||
|
||||
## Authorization Notes
|
||||
|
||||
Only dispatcher sessions can enter dispatch mode. If the hydrated session is
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user