diff --git a/.gitignore b/.gitignore index fdf6c63..751ea54 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,3 @@ Thumbs.db arma/ui/map-viewer/ arma/server/surrealdb/forge.db/ promo/ -arma/forge_pmc_simulator.Tanoa/ diff --git a/arma/forge_pmc_simulator.Tanoa/CfgEnemyFactions.hpp b/arma/forge_pmc_simulator.Tanoa/CfgEnemyFactions.hpp new file mode 100644 index 0000000..fe760df --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/CfgEnemyFactions.hpp @@ -0,0 +1,41 @@ +/* + * Enemy faction options for the PMC simulator setup flow. + * + * Consumers: + * - forge_pmc_fnc_getEnemyFactionOptions reads this config for the startup UI. + * - forge_pmc_fnc_resolveEnemyFactionParam maps mission param values back to + * faction classnames during fallback/default setup. + * - Mission generators use ENEMY_FACTION_STR and ENEMY_SIDE after setup has + * applied the selected option. + * + * Keep this list aligned with the server modpack. Classnames that are not + * present in CfgFactionClasses may still appear in the UI, but downstream unit + * pool generation may fall back or fail to find units. + * + * BLUFOR/WEST factions are intentionally omitted because generated missions use + * these options as opposing forces. + */ +class CfgEnemyFactions { + /* + * Option format: + * - class name: stable config entry name, normally matching the faction. + * - value: legacy mission param numeric value. + * - faction: CfgFactionClasses classname used for spawning. + * - display: user-facing label shown in the setup UI. + */ + class Options { + class OPF_F { value = 0; faction = "OPF_F"; display = "CSAT"; }; + class OPF_T_F { value = 1; faction = "OPF_T_F"; display = "CSAT (Pacific)"; }; + class OPF_V_F { value = 2; faction = "OPF_V_F"; display = "Viper"; }; + class OPF_R_F { value = 3; faction = "OPF_R_F"; display = "Spetnaz"; }; + class OPF_SFIA_lxWS { value = 4; faction = "OPF_SFIA_lxWS"; display = "SFIA"; }; + class OPF_TURA_lxWS { value = 5; faction = "OPF_TURA_lxWS"; display = "Tura"; }; + class IND_F { value = 6; faction = "IND_F"; display = "AAF"; }; + class IND_G_F { value = 7; faction = "IND_G_F"; display = "FIA"; }; + class IND_E_F { value = 8; faction = "IND_E_F"; display = "LDF"; }; + class IND_C_F { value = 9; faction = "IND_C_F"; display = "Syndikat"; }; + class IND_L_F { value = 10; faction = "IND_L_F"; display = "Looters"; }; + class IND_SFIA_lxWS { value = 11; faction = "IND_SFIA_lxWS"; display = "SFIA"; }; + class IND_TURA_lxWS { value = 12; faction = "IND_TURA_lxWS"; display = "Tura"; }; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/CfgFactionUnitMap.hpp b/arma/forge_pmc_simulator.Tanoa/CfgFactionUnitMap.hpp new file mode 100644 index 0000000..c06b075 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/CfgFactionUnitMap.hpp @@ -0,0 +1,31 @@ +/* + * Optional faction-to-unit template map. + * + * Current behavior: + * - Enemy unit pools are primarily built from CfgGroups/CfgVehicles through + * forge_pmc_fnc_getEnemyFactionUnitPool. + * - This config is a template for deterministic per-faction unit pools if the + * automatic faction lookup is not specific enough for a server/modpack. + * + * To enable this map, wire it into forge_pmc_fnc_getEnemyFactionUnitPool or the + * generator spawn path before falling back to config traversal. + */ +class CfgFactionUnitMap { + /* + * Mapping key should match the selected faction classname from + * CfgEnemyFactions.Options[].faction, such as "IND_G_F". + */ + class IND_G_F { + /* + * Unit template fields: + * - vehicle: unit classname to spawn. + * - rank: Arma rank string applied after spawn. + * - position[]: base local offset from the generated mission position. + * + * Generators may add small random jitter to the position offset. + */ + class Units { + class Unit0 { vehicle = "Ind_G_Unit1_F"; rank = "SERGEANT"; position[] = {0,0,0}; }; + }; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/CfgFunctions.hpp b/arma/forge_pmc_simulator.Tanoa/CfgFunctions.hpp new file mode 100644 index 0000000..797d23c --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/CfgFunctions.hpp @@ -0,0 +1,45 @@ +class CfgFunctions { + class forge_pmc { + tag = "forge_pmc"; + + class helpers { + file = "functions\helpers"; + class getAllEnemyFactions {}; + class getEnemyFactionListboxSelection {}; + class getEnemyFactionOptions {}; + class getEnemyFactionSide {}; + class getEnemyFactionUnitPool {}; + class getMissionSettingRange {}; + class populateEnemyFactionListbox {}; + class resolveEnemyFactionParam {}; + }; + + class missionSetup { + file = "functions\missionSetup"; + class handleMissionSetupUIEvents {}; + class openMissionSetupUI {}; + class setupMenu_applySettings {}; + }; + + class missionManager { + file = "functions\missionManager"; + class missionManager { + postInit = 1; + }; + class persistentCadMissionManager {}; + class updateEnemyCountFromActivePlayers {}; + }; + + class missionGenerators { + file = "functions\missionGenerators"; + class attackMissionGenerator {}; + class defendMissionGenerator {}; + class destroyMissionGenerator {}; + class deliveryMissionGenerator {}; + class defuseMissionGenerator {}; + class hostageMissionGenerator {}; + class hvtMissionGenerator {}; + class captureHvtMissionGenerator {}; + }; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/CfgMissions.hpp b/arma/forge_pmc_simulator.Tanoa/CfgMissions.hpp new file mode 100644 index 0000000..da04f0d --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/CfgMissions.hpp @@ -0,0 +1,223 @@ +/* + * 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: + * - Values in this config provide defaults for the mission setup UI. + * - If the setup UI is cancelled, Arma mission 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 { + // Maximum number of generated missions allowed to be active at once. + maxConcurrentMissions = 3; + + // 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; + hvtkill = 0.15; + hvtcapture = 0.15; + defuse = 0.15; + delivery = 0.1; + destroy = 0.2; + }; + + /* + * Mission type settings. + * + * Common fields: + * - Rewards.money[]: min/max funds reward. + * - Rewards.reputation[]: min/max reputation reward. + * - Rewards.[]: item reward rolls as {classname, chance}. + * - penalty[]: min/max reputation penalty on failure. + * - timeLimit[]: min/max task time limit in seconds. + */ + class MissionTypes { + // Search-and-destroy infantry engagement. + class Attack { + minUnits = 4; + maxUnits = 8; + class Rewards { + money[] = {25000, 60000}; + reputation[] = {6, 14}; + 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[] = {-8, -3}; + 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}; + reputation[] = {8, 18}; + 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[] = {-12, -4}; + timeLimit[] = {300, 1800}; + }; + + // Rescue a hostage from a generated hostile site. + class Hostage { + // Candidate hostage classnames by broad source category. + class Hostages { + 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}; + reputation[] = {12, 25}; + 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[] = {-16, -6}; + timeLimit[] = {600, 900}; + }; + + // 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}; + 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}; + }; + + // 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"}; + }; + // 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}; + 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[] = {-9, -3}; + timeLimit[] = {600, 900}; + }; + + // Deliver cargo or vehicles between generated locations. + class Delivery { + // Candidate delivery objects grouped by cargo type. + class Cargo { + 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[] = {"B_MRAP_01_F", "B_Truck_01_transport_F", "B_Heli_Transport_03_F", "B_Heli_Transport_03_unarmed_F", "B_Heli_Light_01_F", "B_Heli_Transport_01_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}; + }; + + // 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}; + }; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/CfgParams.hpp b/arma/forge_pmc_simulator.Tanoa/CfgParams.hpp new file mode 100644 index 0000000..bb6915c --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/CfgParams.hpp @@ -0,0 +1,82 @@ +class Params { + class enemyFaction { + title = "Enemy Faction"; + values[] = {0,1,2,3,4,5,6,7,8,9,10,11,12}; + texts[] = { + "CSAT", + "CSAT (Pacific)", + "Viper", + "Spetnaz", + "SFIA (OPFOR)", + "Tura (OPFOR)", + "AAF", + "FIA", + "LDF", + "Syndikat", + "Looters", + "SFIA (Independent)", + "Tura (Independent)" + }; + default = 7; + }; + + class maxConcurrentMissions { + title = "Max Concurrent Missions"; + values[] = {1,2,3,4,5}; + default = 3; + }; + + class missionInterval { + title = "Mission Interval (seconds)"; + values[] = {60,120,300,600,900}; + default = 300; + }; + + class moneyMin { + title = "Money Min"; + values[] = {0,10000,25000,40000}; + default = 25000; + }; + + class moneyMax { + title = "Money Max"; + values[] = {20000,40000,60000,80000,120000}; + default = 60000; + }; + + class reputationMin { + title = "Reputation Min"; + values[] = {0,2,4,6,8,10}; + default = 6; + }; + + class reputationMax { + title = "Reputation Max"; + values[] = {10,12,14,18,22,30}; + default = 14; + }; + + class penaltyMin { + title = "Penalty Min"; + values[] = {-20,-16,-12,-8,-6,-4,-3}; + default = -8; + }; + + class penaltyMax { + title = "Penalty Max"; + values[] = {-20,-16,-12,-8,-6,-4,-3}; + default = -3; + }; + + class timeLimitMin { + title = "Time Limit Min (seconds)"; + values[] = {300,600,900,1200,1800}; + default = 900; + }; + + class timeLimitMax { + title = "Time Limit Max (seconds)"; + values[] = {600,900,1800,2400,3600}; + default = 1800; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/TODO.md b/arma/forge_pmc_simulator.Tanoa/TODO.md new file mode 100644 index 0000000..745664c --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/TODO.md @@ -0,0 +1,136 @@ +Mission updates + +Implemented in mission sandbox: +- Mission manager now skips unavailable generators instead of throwing undefined-variable errors. +- Enemy-count scaling now executes through `forge_pmc_fnc_updateEnemyCountFromActivePlayers` and is applied by generated enemy spawns. +- HVT kill/capture generators no longer call an undefined patrol method; they spawn the HVT plus escorts directly. +- Capture HVT now publishes `CaptureHvtMissionGenerator` without overwriting `HvtMissionGenerator`. +- Delivery zones are placed at a random safe map location away from the cargo pickup. +- Destroy missions prefer nearby map-placed configured target buildings before spawning a fallback target. +- Defuse, Hostage, and HVT generators can use nearby building positions where available. + +- ~~all missions require player count check when spawning~~ + - Added `forge_pmc_fnc_updateEnemyCountFromActivePlayers` execution to generated enemy spawn paths. Attack, Defend, Defuse, Destroy, Hostage, HVT Kill, and HVT Capture now apply the active-player multiplier where they spawn enemy units or escorts. + +{Mission manager} +Wants: +- ~~Prevent a missing/failed generator from breaking mission manager startup.~~ + - Mission manager now builds the generator list from variable names and skips unavailable/invalid generators with a warning. +- ~~Support both HVT mission variants in weighted selection.~~ + - Registered generator keys now match `CfgMissions` weights: `hvtkill` and `hvtcapture`. + +Errors: +~~Resolved: undefined `DefuseMissionGenerator` could break mission manager startup.~~ +```15:57:53 Error in expression +15:57:53 Error position: +15:57:53 Error Undefined variable in expression: defusemissiongenerator +15:57:53 File C:\Users\cypha\OneDrive\Documents\Arma 3 - Other Profiles\MrPakeha\mpmissions\Forge\forge_pmc_simulator_v2.Tanoa\functions\fn_missionManager.sqf..., line 67 +``` + +------------------------------------------------------------------------------------------------- +{Attack} +Wants: +- ~~Apply active-player enemy count scaling.~~ + - Attack generated patrol size now uses the active-player multiplier. + +Errors: + +------------------------------------------------------------------------------------------------- +{Defence} +Wants: +- ~~Apply active-player enemy count scaling.~~ + - Defend wave template size now uses the active-player multiplier. + +Errors: + +------------------------------------------------------------------------------------------------- +{Defuse} +Wants: +- ~~add building to possible spawnable locations~~ + - Defuse protected object/device placement now attempts nearby building positions before falling back outdoors. +- ~~Apply active-player enemy count scaling.~~ + - Defuse patrol size now uses the active-player multiplier. + +Errors: +~~Resolved: invalid `exitWith {0}` syntax in building-position selection.~~ +```- 15:58:01 Error in expression +15:58:01 Error position: <{0}; +}; +}; +}; + +private _protectedPos = [> +15:58:01 Error Missing ; +15:58:01 File C:\Users\cypha\OneDrive\Documents\Arma 3 - Other Profiles\MrPakeha\mpmissions\Forge\forge_pmc_simulator_v2.Tanoa\missionGenerator\fn_defuseMissionGenerator.sqf..., line 321 +``` + +------------------------------------------------------------------------------------------------- +{Delivery} +Wants: +- ~~add code to move marker to random location around the map~~ + - Delivery zone marker now moves to a random safe map location away from the cargo pickup, with fallback placement if a safe position cannot be found. + +Errors: +untested + +Changes: +- added defined location for spawning cargo +-- logic chain: spawn near "cargoSpawn", if not found, spawn near "ExtZone", if not found, revert to original offset +- added random destination-zone placement around the map + +------------------------------------------------------------------------------------------------- +{Destroy} +Wants: +- ~~add possibility of map placed building as destroyable targets~~ + - Destroy now prefers nearby map-placed configured target objects before spawning a fallback target. +- ~~Apply active-player enemy count scaling.~~ + - Destroy patrol size now uses the active-player multiplier. + +Errors: +untested + +------------------------------------------------------------------------------------------------- +{Hostage} +Wants: +- ~~add building as possible spawning location~~ + - Hostages and shooters now receive nearby building positions from location selection and use them when available. +- ~~have code look for marker "extMarker", if not found, find safe location within "blkMarker", if that fails, find safe location >2km away from task~~ + - Hostage extraction already searches `ExtZone`, falls back through blacklist-area safe placement, then falls back to a safe location over 2km from the task. +- ~~Apply active-player enemy count scaling.~~ + - Hostage patrol size now uses the active-player multiplier. + +Errors: +untested + +Changes: +- added a search for ExtZone +- if "ExtZone" is not found, will seach for a safe location within "blklist", if that fails, find a safe location >2km from task. +- Will attempt to spawn shooter and hostage inside a building if possible. +------------------------------------------------------------------------------------------------- +{HVT} +Wants: +- ~~add buildings as a possible spawning location~~ + - HVT Kill and HVT Capture now gather nearby building positions and use them for the HVT/escort spawn when available. +- ~~have code look for marker "extMarker", if not found find safe location within "blkMarker", if that fails find safe location >2km away from task~~ + - HVT Kill and HVT Capture extraction zones now search `ExtZone`/`extMarker`, fall back through `blklist`/`blkMarker`, then fall back to a safe location over 2km from the task. +- ~~Apply active-player enemy count scaling.~~ + - HVT escort count now uses the active-player multiplier. +- ~~Keep HVT Kill and HVT Capture as separate generators.~~ + - Capture HVT now publishes `CaptureHvtMissionGenerator` instead of overwriting `HvtMissionGenerator`. + +Errors: +untested + +Changes: +- created a second hvt mission that defines a capture mission instead of kill +- fixed HVT generators calling an undefined patrol method diff --git a/arma/forge_pmc_simulator.Tanoa/cba_settings.sqf b/arma/forge_pmc_simulator.Tanoa/cba_settings.sqf new file mode 100644 index 0000000..458631c --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/cba_settings.sqf @@ -0,0 +1,5 @@ +force forge_client_actor_enableLoc = false; +force forge_client_actor_enableGear = true; +force forge_client_actor_enableVA = true; +force forge_client_actor_enableVG = true; +force forge_task_enableGenerator = false; diff --git a/arma/forge_pmc_simulator.Tanoa/config.h b/arma/forge_pmc_simulator.Tanoa/config.h new file mode 100644 index 0000000..531dce8 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/config.h @@ -0,0 +1 @@ +#include "CfgFunctions.h" diff --git a/arma/forge_pmc_simulator.Tanoa/description.ext b/arma/forge_pmc_simulator.Tanoa/description.ext new file mode 100644 index 0000000..2c899e1 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/description.ext @@ -0,0 +1,41 @@ +author = "IDSolutions"; +onLoadName = "FORGE - Dev Environment v2.0"; +onLoadMission = "A dev environment for the FORGE Framework"; +loadScreen = ""; + +class Header { + gameType = "Sandbox"; + minPlayers = 1; + maxPlayers = 22; +}; + +respawn = 3; +respawnButton = 1; +respawnDelay = 5; +respawnDialog = 0; + +disabledAI = 1; +enableDebugConsole = 1; +enableTargetDebug = 1; + +allowProfileGlasses = 0; +cba_settings_hasSettingsFile = 1; +corpseManagerMode = 0; + +#include "CfgParams.hpp" +#include "CfgEnemyFactions.hpp" +#include "CfgMissions.hpp" +#include "CfgFunctions.hpp" +#include "ui\baseControls.hpp" +#include "ui\MissionSetup.hpp" + +class CfgRemoteExec { + class Functions { + mode = 1; + jip = 0; + + class forge_pmc_fnc_setupMenu_applySettings { + allowedTargets = 2; + }; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/README.md b/arma/forge_pmc_simulator.Tanoa/functions/helpers/README.md new file mode 100644 index 0000000..343278f --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/README.md @@ -0,0 +1,15 @@ +# Helper Functions + +Helper functions provide reusable lookups and conversions for the PMC simulator mission. + +## Registered Functions +- `forge_pmc_fnc_getAllEnemyFactions` returns available non-BLUFOR faction classnames. +- `forge_pmc_fnc_getEnemyFactionOptions` reads configured faction options from `CfgEnemyFactions`. +- `forge_pmc_fnc_resolveEnemyFactionParam` converts a mission parameter value into a faction classname. +- `forge_pmc_fnc_getEnemyFactionSide` resolves a faction classname to an Arma side. +- `forge_pmc_fnc_getEnemyFactionUnitPool` builds the unit pool used by generated enemy spawns. +- `forge_pmc_fnc_getMissionSettingRange` resolves generator ranges from applied mission settings with config fallbacks. +- `forge_pmc_fnc_getEnemyFactionListboxSelection` and `forge_pmc_fnc_populateEnemyFactionListbox` support faction picker UI/listbox flows. + +## Notes +These functions are registered under the `forge_pmc` tag, so their public names do not include the folder name. Moving a helper within this folder should not change callers as long as `CfgFunctions.hpp` remains updated. diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getAllEnemyFactions.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getAllEnemyFactions.sqf new file mode 100644 index 0000000..f505977 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getAllEnemyFactions.sqf @@ -0,0 +1,62 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Returns candidate enemy faction classnames available to the mission. + * This is a runtime helper only; description.ext mission params still use + * the static CfgEnemyFactions list. + * + * Arguments: + * 0: Exclude BLUFOR/WEST factions (Default: true) + * 1: Required faction side text (Default: "") + * + * Return Value: + * Enemy faction classnames + * + * Public: No + */ + +params [ + ["_excludeBLUFOR", true], + ["_minSide", ""] +]; + +private _out = []; + +// Candidate sources (may include duplicates and may contain non-faction entries depending on mods) +private _candidates = []; + +// Modded missions often populate this; safe fallback to config traversal. +{ + if (_x isEqualType "" && { _x != "" }) then { _candidates pushBackUnique _x; }; +} forEach [ + "OPF_F", "IND_F" // common containers; harmless if not present in this context +]; + +// Traverse side->factions in config when possible. +// We use configClasses on CfgFactionClasses. +private _cfg = configFile >> "CfgFactionClasses"; +if (isClass _cfg) then { + { + private _factionClass = configName _x; + private _sideText = toUpperANSI getText (_x >> "side"); + if (_factionClass isEqualType "" && { _factionClass != "" }) then { + if (_excludeBLUFOR) then { + // Common side strings: "WEST", "EAST", "GUER" + if (_sideText == "WEST") then { continue; }; + }; + + if (_minSide != "") then { + if (_sideText != toUpperANSI _minSide) then { continue; }; + }; + + _out pushBackUnique _factionClass; + }; + } forEach ("true" configClasses _cfg); +}; + +// Fallback: if config traversal failed, at least return known keys used by the mission. +if (_out isEqualTo []) then { + _out = ["OPF_F", "IND_G_F"]; + _out pushBackUnique "IND_G_F"; +}; + +_out diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionListboxSelection.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionListboxSelection.sqf new file mode 100644 index 0000000..8d799f5 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionListboxSelection.sqf @@ -0,0 +1,40 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Reads the selected enemy faction classname from a populated listbox or + * combo control. + * + * Arguments: + * 0: Listbox/combo control or display + * 1: Control IDC when parameter 0 is a display (Default: -1) + * 2: Fallback faction classname (Default: "IND_G_F") + * + * Return Value: + * Selected faction classname + * + * Public: No + */ + +params [ + ["_controlOrDisplay", controlNull, [controlNull, displayNull]], + ["_idc", -1, [0]], + ["_fallback", "IND_G_F", [""]] +]; + +private _control = controlNull; +if (_controlOrDisplay isEqualType displayNull) then { + if (_idc >= 0) then { + _control = _controlOrDisplay displayCtrl _idc; + }; +} else { + _control = _controlOrDisplay; +}; + +if (isNull _control) exitWith { _fallback }; + +private _index = lbCurSel _control; +if (_index < 0) exitWith { _fallback }; + +private _faction = _control lbData _index; +if (_faction isEqualTo "") exitWith { _fallback }; + +_faction diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionOptions.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionOptions.sqf new file mode 100644 index 0000000..692b476 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionOptions.sqf @@ -0,0 +1,52 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Reads enemy faction options from missionConfigFile >> CfgEnemyFactions + * >> Options for setup UI hydration and mission param fallback resolution. + * + * Arguments: + * None + * + * Return Value: + * Enemy faction options as [factionClassname, displayName, paramValue] + * + * Public: No + */ + +private _optionsConfig = missionConfigFile >> "CfgEnemyFactions" >> "Options"; +private _options = []; + +if (isClass _optionsConfig) then { + { + private _configName = configName _x; + private _value = getNumber (_x >> "value"); + private _faction = getText (_x >> "faction"); + private _display = getText (_x >> "display"); + + if (_faction isEqualTo "") then { + _faction = _configName; + }; + + if (!isClass (configFile >> "CfgFactionClasses" >> _faction) && { + isClass (configFile >> "CfgFactionClasses" >> _configName) + }) then { + _faction = _configName; + }; + + if (_display isEqualTo "") then { + _display = _faction; + }; + + if (_faction isNotEqualTo "") then { + _options pushBack [_faction, _display, _value]; + }; + } forEach ("true" configClasses _optionsConfig); +}; + +if (_options isEqualTo []) then { + _options = [ + ["OPF_F", "CSAT", 0], + ["IND_G_F", "FIA", 7] + ]; +}; + +_options diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionSide.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionSide.sqf new file mode 100644 index 0000000..3b78fb2 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionSide.sqf @@ -0,0 +1,57 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Resolves a faction classname to an Arma side. Prefers CfgFactionClasses + * data and falls back to common faction-prefix conventions. + * + * Arguments: + * 0: Enemy faction classname + * 1: Fallback side (Default: east) + * + * Return Value: + * Resolved side + * + * Public: No + */ + +params [ + ["_enemyFaction", ""], + ["_fallbackSide", east] +]; + +private _f = toUpperANSI _enemyFaction; +if (_f isEqualTo "") exitWith { _fallbackSide }; + +// Try CfgFactionClasses first. +private _side = _fallbackSide; +private _cfgFaction = configFile >> "CfgFactionClasses" >> _enemyFaction; +if (isClass _cfgFaction) then { + private _sideNumber = getNumber (_cfgFaction >> "side"); + private _sideText = toUpperANSI getText (_cfgFaction >> "side"); + + if (_sideNumber > 0 || { _sideText isEqualTo "0" }) then { + switch (_sideNumber) do { + case 0: { _side = east; }; + case 2: { _side = resistance; }; + case 1: { _side = _fallbackSide; }; // BLUFOR excluded + default { _side = _fallbackSide; }; + }; + } else { + switch (_sideText) do { + case "EAST": { _side = east; }; + case "OPFOR": { _side = east; }; + case "GUER": { _side = resistance; }; + case "GUERRILA": { _side = resistance; }; + case "GUERRILLA": { _side = resistance; }; + case "INDEPENDENT": { _side = resistance; }; + case "RESISTANCE": { _side = resistance; }; + case "WEST": { _side = _fallbackSide; }; // BLUFOR excluded + default { _side = _fallbackSide; }; + }; + }; +} else { + // Fallback: try to infer via mission CfgMissions AIGroups unit side fields. + // Since the current spawn system uses side, this is good enough. + // If faction isn't found, keep fallback. +}; + +_side diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionUnitPool.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionUnitPool.sqf new file mode 100644 index 0000000..931f1a2 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getEnemyFactionUnitPool.sqf @@ -0,0 +1,70 @@ +/* + * 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 (Default: ENEMY_FACTION_STR or "IND_G_F") + * 1: Fallback side (Default: ENEMY_SIDE or east) + * + * Return Value: + * Unit definitions with vehicle, rank, and position keys + * + * Public: No + */ + +params [ + ["_faction", missionNamespace getVariable ["ENEMY_FACTION_STR", "IND_G_F"], [""]], + ["_fallbackSide", missionNamespace getVariable ["ENEMY_SIDE", east], [east]] +]; + +if (_faction isEqualTo "") then { + _faction = "IND_G_F"; +}; + +private _pool = []; +private _sideNumber = [_fallbackSide] call BIS_fnc_sideID; + +{ + if (getNumber (_x >> "scope") < 2) then { continue; }; + if (getText (_x >> "faction") isNotEqualTo _faction) 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 []) 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", if (_forEachIndex == 0) then { "SERGEANT" } else { "PRIVATE" }], + ["position", [0, 0, 0]] + ]; + } forEach _fallbackUnits; +}; + +_pool diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getMissionSettingRange.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getMissionSettingRange.sqf new file mode 100644 index 0000000..7189fd1 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_getMissionSettingRange.sqf @@ -0,0 +1,49 @@ +/* + * 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 + * 1: Config path segments to the range array + * 2: Mission settings min key + * 3: Mission settings max key + * 4: Fallback [min, max] range (Default: [0, 0]) + * + * Return Value: + * Normalized [min, max] range + * + * 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 { + _max = _min; +}; + +[_min, _max] diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_populateEnemyFactionListbox.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_populateEnemyFactionListbox.sqf new file mode 100644 index 0000000..db1ead5 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_populateEnemyFactionListbox.sqf @@ -0,0 +1,58 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Populates a listbox or combo control with CfgEnemyFactions options. + * + * Arguments: + * 0: Listbox/combo control or display + * 1: Control IDC when parameter 0 is a display (Default: -1) + * 2: Selected faction classname (Default: current enemy faction) + * + * Return Value: + * Number of options added + * + * Public: No + */ + +params [ + ["_controlOrDisplay", controlNull, [controlNull, displayNull]], + ["_idc", -1, [0]], + ["_selectedFaction", missionNamespace getVariable ["ENEMY_FACTION_STR", missionNamespace getVariable ["enemyFaction", "IND_G_F"]], [""]] +]; + +private _control = controlNull; +if (_controlOrDisplay isEqualType displayNull) then { + if (_idc >= 0) then { + _control = _controlOrDisplay displayCtrl _idc; + }; +} else { + _control = _controlOrDisplay; +}; + +if (isNull _control) exitWith { 0 }; + +lbClear _control; + +private _selectedIndex = -1; +private _options = [] call forge_pmc_fnc_getEnemyFactionOptions; + +{ + _x params ["_faction", "_display"]; + + private _index = _control lbAdd _display; + _control lbSetData [_index, _faction]; + _control lbSetTooltip [_index, _faction]; + + if (_faction isEqualTo _selectedFaction) then { + _selectedIndex = _index; + }; +} forEach _options; + +if (_selectedIndex < 0 && { count _options > 0 }) then { + _selectedIndex = 0; +}; + +if (_selectedIndex >= 0) then { + _control lbSetCurSel _selectedIndex; +}; + +count _options diff --git a/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_resolveEnemyFactionParam.sqf b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_resolveEnemyFactionParam.sqf new file mode 100644 index 0000000..eb651e3 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/helpers/fn_resolveEnemyFactionParam.sqf @@ -0,0 +1,35 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Resolves a mission param value or faction classname into the selected + * enemy faction classname. + * + * Arguments: + * 0: Param value from Params::enemyFaction (Default: 7) + * 1: Fallback faction classname (Default: "IND_G_F") + * + * Return Value: + * Faction classname + * + * Public: No + */ + +params [ + ["_value", 7, [0, ""]], + ["_fallback", "IND_G_F", [""]] +]; + +if (_value isEqualType "") then { + if (_value isEqualTo "") exitWith { _fallback }; + if (isClass (configFile >> "CfgFactionClasses" >> _value)) exitWith { _value }; + _value = parseNumber _value; +}; + +private _faction = _fallback; +{ + _x params ["_optionFaction", "_display", "_optionValue"]; + if (_optionValue isEqualTo _value) exitWith { + _faction = _optionFaction; + }; +} forEach ([] call forge_pmc_fnc_getEnemyFactionOptions); + +_faction diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/README.md b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/README.md new file mode 100644 index 0000000..93778a2 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/README.md @@ -0,0 +1,29 @@ +# Mission Generators + +This folder contains per-mission-type dynamic mission generators for the PMC simulator mission. + +## Naming +- `fn_MissionGenerator.sqf` + +## Task usage +Generators follow the same runtime shape as the original attack generator: +- Base class stored as a HashMap-backed "object" with compiled functions. +- Provides: `startMission` and `completeMission`. +- Spawns AI/objects appropriate to the mission type and registers tasks via `forge_server_task_fnc_startTask`. + +## Location +Mission functions live under `functions/`; these generator files are registered through `CfgFunctions.hpp` with `file = "functions\missionGenerators"`. + +## Registered Generators +- `forge_pmc_fnc_attackMissionGenerator` +- `forge_pmc_fnc_defendMissionGenerator` +- `forge_pmc_fnc_destroyMissionGenerator` +- `forge_pmc_fnc_deliveryMissionGenerator` +- `forge_pmc_fnc_defuseMissionGenerator` +- `forge_pmc_fnc_hostageMissionGenerator` +- `forge_pmc_fnc_hvtMissionGenerator` +- `forge_pmc_fnc_captureHvtMissionGenerator` + +## Settings +Generators should read shared reward/time ranges through `forge_pmc_fnc_getMissionSettingRange` so the startup UI and mission-param fallback stay consistent. + diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_attackMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_attackMissionGenerator.sqf new file mode 100644 index 0000000..cc453d3 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_attackMissionGenerator.sqf @@ -0,0 +1,413 @@ +/* + * 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: + * N/A. Defines AttackMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +AttackMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "AttackMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "CfgMissions"; + _self set ["missionConfig", _missionConfig]; + _self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")]; + _self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")]; + _self set ["generatorType", "attack"]; + }], + ["getGeneratorType", compileFinal { + _self getOrDefault ["generatorType", "attack"] + }], + ["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 "attack") 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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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", "Attack mission generator: selectLocation failed to find a valid dynamic position."] call forge_server_common_fnc_log; + createHashMap + }; + + createHashMapFromArray [ + ["position", _taskPos], + ["grid", mapGridPosition _taskPos] + ] + }], + ["spawnAttackGroup", 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 forge_pmc_fnc_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 forge_pmc_fnc_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 ["Attack mission generator: selected AI group side '%1' produced an empty unit pool.", _side]] call forge_server_common_fnc_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 BIS_fnc_taskPatrol; + + ["INFO", format [ + "Attack mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4", + _side, + count (units _group), + _patrolRadius, + _position + ]] call forge_server_common_fnc_log; + _group + }], + ["rollRewards", compileFinal { + private _attackConfig = _self getOrDefault ["attackConfig", 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 (_attackConfig >> "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 _attackConfig = _self getOrDefault ["attackConfig", 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 [ + "Attack mission generator: selected location. Grid=%1, Position=%2", + _grid, + _position + ]] call forge_server_common_fnc_log; + + private _group = _self call ["spawnAttackGroup", [_position]]; + if (isNull _group) exitWith { + ["WARNING", format [ + "Attack mission generator: spawnAttackGroup failed for Grid=%1, Position=%2", + _grid, + _position + ]] call forge_server_common_fnc_log; + "" + }; + + private _units = units _group; + if (_units isEqualTo []) exitWith { + ["WARNING", format [ + "Attack mission generator: spawned group has no units. Grid=%1, Group=%2", + _grid, + _group + ]] call forge_server_common_fnc_log; + deleteGroup _group; + "" + }; + + private _taskID = format ["task_attack_%1", round (diag_tickTime * 1000)]; + private _rewardRange = [_attackConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [25000, 60000]] call forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_attackConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [6, 14]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_attackConfig, ["penalty"], "penaltyMin", "penaltyMax", [-8, -3]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_attackConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_getMissionSettingRange; + private _rewards = _self call ["rollRewards"]; + private _fundsReward = _rewardRange call BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_randomNum; + + ["INFO", format [ + "Attack mission generator: creating task. TaskID=%1, Grid=%2, Units=%3", + _taskID, + _grid, + count _units + ]] call forge_server_common_fnc_log; + + private _success = [ + "attack", + _taskID, + _position, + format ["Attack: Grid %1", _grid], + format ["Eliminate hostile forces operating near grid %1.", _grid], + createHashMapFromArray [["targets", _units]], + createHashMapFromArray [ + ["limitFail", 0], + ["limitSuccess", count _units], + ["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 forge_server_task_fnc_startTask; + + if !(_success) exitWith { + ["WARNING", format [ + "Attack mission generator: startTask failed. TaskID=%1, Grid=%2, Units=%3", + _taskID, + _grid, + count _units + ]] call forge_server_common_fnc_log; + "" + }; + + ["INFO", format [ + "Attack mission generator: task registered. TaskID=%1, Source=mission_manager, TimeLimit=%2s, LimitSuccess=%3", + _taskID, + _timeLimit, + count _units + ]] call forge_server_common_fnc_log; + + private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap]; + _activeMissionRegistry set [_taskID, createHashMapFromArray [ + ["generatorType", _self call ["getGeneratorType", []]], + ["position", _position], + ["startedAt", serverTime] + ]]; + _manager set ["activeMissionRegistry", _activeMissionRegistry]; + + ["INFO", format [ + "Attack mission generator: mission started successfully. TaskID=%1, Grid=%2", + _taskID, + _grid + ]] call forge_server_common_fnc_log; + + _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", []]; + _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 + }] +]; +publicVariable "AttackMissionGeneratorBaseClass"; + +AttackMissionGenerator = createHashMapObject [AttackMissionGeneratorBaseClass]; +publicVariable "AttackMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_captureHvtMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_captureHvtMissionGenerator.sqf new file mode 100644 index 0000000..d1730c9 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_captureHvtMissionGenerator.sqf @@ -0,0 +1,440 @@ +/* + * 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 CaptureHvtMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +CaptureHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "CaptureHvtMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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 forge_server_common_fnc_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 forge_pmc_fnc_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 forge_pmc_fnc_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 BIS_fnc_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 forge_server_common_fnc_log; + + private _taskID = format ["task_capture_hvt_%1", round (diag_tickTime * 1000)]; + private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_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 BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_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 BIS_fnc_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 BIS_fnc_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 setMarkerShape "ELLIPSE"; + _extZone setMarkerSize [160, 160]; + _extZone setMarkerText format ["HVT Extraction Zone %1", _grid]; + + 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 forge_server_task_fnc_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 + }] +]; +publicVariable "CaptureHvtMissionGeneratorBaseClass"; + +CaptureHvtMissionGenerator = createHashMapObject [CaptureHvtMissionGeneratorBaseClass]; +publicVariable "CaptureHvtMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_defendMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_defendMissionGenerator.sqf new file mode 100644 index 0000000..2d4f7e2 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_defendMissionGenerator.sqf @@ -0,0 +1,380 @@ +/* + * 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 DefendMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +DefendMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "DefendMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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 forge_server_common_fnc_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 forge_pmc_fnc_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 forge_pmc_fnc_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 forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_defendConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [8, 18]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_defendConfig, ["penalty"], "penaltyMin", "penaltyMax", [-12, -4]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_defendConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [300, 1800]] call forge_pmc_fnc_getMissionSettingRange; + private _rewards = _self call ["rollRewards"]; + private _enemyTemplates = _self call ["buildDefendTemplateGroups", [_position]]; + if (_enemyTemplates isEqualTo []) exitWith { "" }; + + private _fundsReward = _rewardRange call BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_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 setMarkerShape "ELLIPSE"; + _defenseZone setMarkerSize [25, 25]; + _defenseZone setMarkerText format ["Defense Zone %1", _grid]; + + 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 forge_server_task_fnc_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 + }] +]; +publicVariable "DefendMissionGeneratorBaseClass"; + +DefendMissionGenerator = createHashMapObject [DefendMissionGeneratorBaseClass]; +publicVariable "DefendMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_defuseMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_defuseMissionGenerator.sqf new file mode 100644 index 0000000..481f5fb --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_defuseMissionGenerator.sqf @@ -0,0 +1,510 @@ +/* + * 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 DefuseMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +DefuseMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "DefuseMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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 forge_server_common_fnc_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 forge_pmc_fnc_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 forge_pmc_fnc_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 forge_server_common_fnc_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 BIS_fnc_taskPatrol; + + ["INFO", format [ + "Defuse mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4", + _side, + count (units _group), + _patrolRadius, + _position + ]] call forge_server_common_fnc_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 isEqualTo [])) 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 forge_server_common_fnc_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 forge_server_common_fnc_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 forge_server_common_fnc_log; + deleteGroup _group; + "" + }; + + private _taskID = format ["task_defuse_%1", round (diag_tickTime * 1000)]; + private _rewardRange = [_defuseConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [20000, 50000]] call forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_defuseConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [5, 12]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_defuseConfig, ["penalty"], "penaltyMin", "penaltyMax", [-9, -3]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_defuseConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call forge_pmc_fnc_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 BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_randomNum; + private _iedTimer = 300; + private _targetCount = count _devices; + + private _defuseZone = format ["forge_defuse_zone_%1", _taskID]; + createMarker [_defuseZone, _position]; + _defuseZone setMarkerShape "ELLIPSE"; + _defuseZone setMarkerSize [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 forge_server_task_fnc_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 + }] +]; +publicVariable "DefuseMissionGeneratorBaseClass"; + +DefuseMissionGenerator = createHashMapObject [DefuseMissionGeneratorBaseClass]; +publicVariable "DefuseMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_deliveryMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_deliveryMissionGenerator.sqf new file mode 100644 index 0000000..b676e7a --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_deliveryMissionGenerator.sqf @@ -0,0 +1,359 @@ +/* + * 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 DeliveryMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +DeliveryMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "DeliveryMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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 forge_server_common_fnc_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] + ] + }], + + ["spawnDeliveryCargo", compileFinal { + params [['_position', [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 { [] }; + + private _cargoSpawnObj = objNull; + if (!isNil "cargoSpawn") then { _cargoSpawnObj = cargoSpawn; }; + if (isNull _cargoSpawnObj) then { _cargoSpawnObj = missionNamespace getVariable ["cargoSpawn", objNull]; }; + + private _extZoneObj = objNull; + if (!isNil "ExtZone") then { _extZoneObj = ExtZone; }; + if (isNull _extZoneObj) then { _extZoneObj = missionNamespace getVariable ["ExtZone", objNull]; }; + + for "_i" from 1 to _cargoCount do { + private _cargoClass = selectRandom _cargoPool; + + private _spawnPos = [0, 0, 0]; + if (!isNull _cargoSpawnObj) then { + private _basePos = getPosATL _cargoSpawnObj; + _spawnPos = _basePos vectorAdd [(random 12 - 6), (random 12 - 6), 0]; + } else { + if (!isNull _extZoneObj) then { + private _basePos = getPosATL _extZoneObj; + _spawnPos = _basePos vectorAdd [(random 12 - 6), (random 12 - 6), 0]; + } else { + _spawnPos = _position vectorAdd [(random 80 - 40), (random 80 - 40), 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 _grid = _locationData getOrDefault ["grid", mapGridPosition _position]; + private _taskID = format ["task_delivery_%1", round (diag_tickTime * 1000)]; + private _deliveryZone = format ["forge_delivery_zone_%1", _taskID]; + private _worldSize = worldSize; + private _center = [_worldSize / 2, _worldSize / 2, 0]; + private _deliveryPos = [0, 0, 0]; + private _attempt = 0; + while { _attempt < 80 && { _deliveryPos isEqualTo [0, 0, 0] } } do { + _attempt = _attempt + 1; + private _candidate = [_center, 0, _worldSize / 2 - 1000, 10, 0, 0.3, 0] call BIS_fnc_findSafePos; + if (_candidate isEqualTo [0, 0, 0]) then { continue; }; + if ((_candidate distance2D _position) < 1200) then { continue; }; + _candidate set [2, 0]; + _deliveryPos = _candidate; + }; + if (_deliveryPos isEqualTo [0, 0, 0]) then { + _deliveryPos = [_position, 1200, 2500, 10, 0, 0.3, 0] call BIS_fnc_findSafePos; + }; + if (_deliveryPos isEqualTo [0, 0, 0]) then { + _deliveryPos = _position vectorAdd [1500, 0, 0]; + }; + private _deliveryGrid = mapGridPosition _deliveryPos; + + private _rewardRange = [_deliveryConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [10000, 30000]] call forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_deliveryConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_deliveryConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_deliveryConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_getMissionSettingRange; + private _rewards = _self call ["rollRewards"]; + private _cargoObjects = _self call ["spawnDeliveryCargo", [_position]]; + + if (_cargoObjects isEqualTo []) exitWith { "" }; + + createMarker [_deliveryZone, _deliveryPos]; + _deliveryZone setMarkerShape "ELLIPSE"; + _deliveryZone setMarkerSize [25, 25]; + _deliveryZone setMarkerText format ["Delivery Zone %1", _deliveryGrid]; + + private _fundsReward = _rewardRange call BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_randomNum; + private _cargoCount = count _cargoObjects; + + private _success = [ + "delivery", + _taskID, + _position, + 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 forge_server_task_fnc_startTask; + + if !(_success) exitWith { + deleteMarker _deliveryZone; + "" + }; + + private _activeMissionRegistry = _manager getOrDefault ["activeMissionRegistry", createHashMap]; + _activeMissionRegistry set [_taskID, createHashMapFromArray [ + ["generatorType", _self call ["getGeneratorType", []]], + ["position", _position], + ["markers", [_deliveryZone]], + ["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 + }] +]; +publicVariable "DeliveryMissionGeneratorBaseClass"; + +DeliveryMissionGenerator = createHashMapObject [DeliveryMissionGeneratorBaseClass]; +publicVariable "DeliveryMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_destroyMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_destroyMissionGenerator.sqf new file mode 100644 index 0000000..0ae5e9e --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_destroyMissionGenerator.sqf @@ -0,0 +1,466 @@ +/* + * 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 DestroyMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +DestroyMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "DestroyMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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 forge_server_common_fnc_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 forge_pmc_fnc_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 forge_pmc_fnc_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 forge_server_common_fnc_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 BIS_fnc_taskDefend; + } else { + [_group, _position, _patrolRadius] call BIS_fnc_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 forge_server_common_fnc_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 forge_server_common_fnc_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 forge_server_common_fnc_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 forge_server_common_fnc_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 forge_server_common_fnc_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 forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_destroyConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [3, 8]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_destroyConfig, ["penalty"], "penaltyMin", "penaltyMax", [-6, -2]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_destroyConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_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 BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_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 forge_server_task_fnc_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 + }] +]; +publicVariable "DestroyMissionGeneratorBaseClass"; + +DestroyMissionGenerator = createHashMapObject [DestroyMissionGeneratorBaseClass]; +publicVariable "DestroyMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_hostageMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_hostageMissionGenerator.sqf new file mode 100644 index 0000000..99148dd --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_hostageMissionGenerator.sqf @@ -0,0 +1,644 @@ +/* + * 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 HostageMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +HostageMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "HostageMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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 forge_server_common_fnc_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 forge_pmc_fnc_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 forge_pmc_fnc_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 forge_server_common_fnc_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 BIS_fnc_taskPatrol; + + ["INFO", format [ + "Hostage mission generator: spawned attack group. Side=%1, Units=%2, PatrolRadius=%3, Position=%4", + _side, + count (units _group), + _patrolRadius, + _position + ]] call forge_server_common_fnc_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 forge_pmc_fnc_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 forge_server_common_fnc_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 forge_server_common_fnc_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 forge_server_common_fnc_log; + deleteGroup _group; + "" + }; + + private _taskID = format ["task_hostage_%1", round (diag_tickTime * 1000)]; + private _rewardRange = [_hostageConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [60000, 140000]] call forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_hostageConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [12, 25]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_hostageConfig, ["penalty"], "penaltyMin", "penaltyMax", [-16, -6]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_hostageConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [600, 900]] call forge_pmc_fnc_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 BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_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, 30, 0] call BIS_fnc_findSafePos; + if (!(_safe isEqualTo [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 _candidate = [_position, 2000, 0] call BIS_fnc_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, 2000, 0] call BIS_fnc_findSafePos; + if (_safe isEqualTo [0, 0, 0]) 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 setMarkerShape "ELLIPSE"; + _extZone setMarkerSize [25, 25]; + _extZone setMarkerText format ["Hostage Extraction %1", _grid]; + + 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 forge_server_task_fnc_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 + }] +]; +publicVariable "HostageMissionGeneratorBaseClass"; + +HostageMissionGenerator = createHashMapObject [HostageMissionGeneratorBaseClass]; +publicVariable "HostageMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_hvtMissionGenerator.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_hvtMissionGenerator.sqf new file mode 100644 index 0000000..5effbcb --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionGenerators/fn_hvtMissionGenerator.sqf @@ -0,0 +1,440 @@ +/* + * 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 KillHvtMissionGeneratorBaseClass in missionNamespace. + * + * Public: No + */ + +KillHvtMissionGeneratorBaseClass = compileFinal createHashMapFromArray [ + ["#type", "KillHvtMissionGeneratorBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "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 _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, _worldSize / 2 - _minEdgeDist, _worldSize / 2 - _minEdgeDist, 3, 0, 0.3, 0] call BIS_fnc_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 forge_server_common_fnc_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 forge_pmc_fnc_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 forge_pmc_fnc_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 BIS_fnc_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 forge_server_common_fnc_log; + + private _taskID = format ["task_kill_hvt_%1", round (diag_tickTime * 1000)]; + private _rewardRange = [_hvtConfig, ["Rewards", "money"], "moneyMin", "moneyMax", [50000, 120000]] call forge_pmc_fnc_getMissionSettingRange; + private _reputationRange = [_hvtConfig, ["Rewards", "reputation"], "reputationMin", "reputationMax", [10, 22]] call forge_pmc_fnc_getMissionSettingRange; + private _penaltyRange = [_hvtConfig, ["penalty"], "penaltyMin", "penaltyMax", [-14, -5]] call forge_pmc_fnc_getMissionSettingRange; + private _timeRange = [_hvtConfig, ["timeLimit"], "timeLimitMin", "timeLimitMax", [900, 1800]] call forge_pmc_fnc_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 BIS_fnc_randomNum; + private _reputationReward = _reputationRange call BIS_fnc_randomNum; + private _reputationPenalty = _penaltyRange call BIS_fnc_randomNum; + private _timeLimit = _timeRange call BIS_fnc_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 BIS_fnc_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 BIS_fnc_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 setMarkerShape "ELLIPSE"; + _extZone setMarkerSize [160, 160]; + _extZone setMarkerText format ["HVT Extraction Zone %1", _grid]; + + 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], + ["extractionZone", _extZone], + ["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 forge_server_task_fnc_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 + }] +]; +publicVariable "KillHvtMissionGeneratorBaseClass"; + +HvtMissionGenerator = createHashMapObject [KillHvtMissionGeneratorBaseClass]; +publicVariable "HvtMissionGenerator"; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionManager/README.md b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/README.md new file mode 100644 index 0000000..0137997 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/README.md @@ -0,0 +1,16 @@ +# Mission Manager Functions + +Mission manager functions coordinate dynamic mission generation for `forge_pmc_simulator.Tanoa`. + +## Registered Functions +- `forge_pmc_fnc_missionManager` is postInit and starts the dynamic mission manager after setup settings are applied. +- `forge_pmc_fnc_persistentCadMissionManager` starts the persistent CAD-oriented mission dispatcher. +- `forge_pmc_fnc_updateEnemyCountFromActivePlayers` updates the active-player scaling multiplier used by enemy spawns. + +## Helper Scripts +- `adminActivatePersistentCad.sqf` is an admin/server-console helper that calls `forge_pmc_fnc_persistentCadMissionManager`. + +## Runtime Notes +`forge_pmc_fnc_missionManager` waits for `forge_pmc_missionSettingsApplied` before it creates generator objects. If no UI settings are submitted within the fallback window, it applies mission param defaults and starts generation. + +Generated tasks are registered through `forge_server_task_fnc_startTask` with source `mission_manager`, so they appear in the normal Forge task/CAD lifecycle. diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionManager/adminActivatePersistentCad.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/adminActivatePersistentCad.sqf new file mode 100644 index 0000000..fa473b7 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/adminActivatePersistentCad.sqf @@ -0,0 +1,20 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Admin/server-console helper that activates the persistent CAD mission + * dispatcher once per server session. + * + * Arguments: + * None + * + * Return Value: + * N/A + * + * Public: No + */ + +if !(isServer) exitWith {}; +if (missionNamespace getVariable ["forge_persistentCadDispatcherEnabled", false]) exitWith {}; + +missionNamespace setVariable ["forge_persistentCadDispatcherEnabled", true, true]; + +[] call forge_pmc_fnc_persistentCadMissionManager; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_missionManager.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_missionManager.sqf new file mode 100644 index 0000000..8a26a6a --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_missionManager.sqf @@ -0,0 +1,255 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Mission-side dynamic mission manager. Waits for startup settings, + * creates mission generator objects, and periodically starts generated + * Forge tasks through forge_server_task_fnc_startTask. + * + * Arguments: + * None + * + * Return Value: + * N/A + * + * Public: No + */ + +if !(isServer) exitWith {}; + +// Startup settings are applied by the setup UI. If the UI is cancelled or +// never opens, fall back to mission params after a short grace period. +if !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) exitWith { + if !(missionNamespace getVariable ["forge_pmc_missionManagerStartupPending", false]) then { + missionNamespace setVariable ["forge_pmc_missionManagerStartupPending", true, true]; + + [] spawn { + waitUntil { + sleep 1; + (missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) || { time > 180 } + }; + + if !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) then { + [] call forge_pmc_fnc_setupMenu_applySettings; + }; + + missionNamespace setVariable ["forge_pmc_missionManagerStartupPending", false, true]; + [] call forge_pmc_fnc_missionManager; + }; + }; +}; + +if (missionNamespace getVariable ["forge_pmc_missionManagerStarted", false]) exitWith {}; +missionNamespace setVariable ["forge_pmc_missionManagerStarted", true, true]; + +["INFO", "Mission manager startup requested."] call forge_server_common_fnc_log; + +// Defuse tasks complete through the task store when ACE reports a successful +// explosive defuse against an object tagged with the generated task id. +if !(missionNamespace getVariable ["forge_pmc_defuseAceHandlerRegistered", false]) then { + ["ace_explosives_defuse", { + private _taskID = ""; + { + if (_x isEqualType objNull && { !isNull _x }) then { + _taskID = _x getVariable ["assignedTask", ""]; + if (_taskID isNotEqualTo "") exitWith {}; + }; + } forEach _this; + + if (_taskID isEqualTo "") exitWith {}; + if (!isNil "forge_server_task_TaskStore") then { + forge_server_task_TaskStore call ["incrementDefuseCount", [_taskID]]; + }; + }] call CBA_fnc_addEventHandler; + + missionNamespace setVariable ["forge_pmc_defuseAceHandlerRegistered", true, true]; +}; + +// Compile each generator before the manager object builds its weighted list. +[] call forge_pmc_fnc_attackMissionGenerator; +[] call forge_pmc_fnc_defendMissionGenerator; +[] call forge_pmc_fnc_defuseMissionGenerator; +[] call forge_pmc_fnc_deliveryMissionGenerator; +[] call forge_pmc_fnc_destroyMissionGenerator; +[] call forge_pmc_fnc_hostageMissionGenerator; +[] call forge_pmc_fnc_hvtMissionGenerator; +[] call forge_pmc_fnc_captureHvtMissionGenerator; + +MissionManagerBaseClass = compileFinal createHashMapFromArray [ + ["#type", "MissionManagerBaseClass"], + ["#create", compileFinal { + private _missionConfig = missionConfigFile >> "CfgMissions"; + private _settings = missionNamespace getVariable ["forge_pmc_missionSettings", createHashMap]; + private _maxConcurrent = _settings getOrDefault ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")]; + private _missionInterval = _settings getOrDefault ["missionInterval", getNumber (_missionConfig >> "missionInterval")]; + + _self set ["missionConfig", _missionConfig]; + _self set ["maxConcurrentMissions", _maxConcurrent]; + _self set ["missionInterval", _missionInterval]; + _self set ["lastMissionGenerationAt", -1e10]; + _self set ["recentLocationRegistry", []]; + _self set ["activeMissionRegistry", createHashMap]; + + private _generators = []; + { + _x params ["_type", "_variableName"]; + if (isNil _variableName) then { + ["WARNING", format ["Mission manager skipped unavailable generator '%1' (%2).", _type, _variableName]] call forge_server_common_fnc_log; + continue; + }; + + private _generator = missionNamespace getVariable [_variableName, createHashMap]; + if (_generator isEqualTo createHashMap) then { + ["WARNING", format ["Mission manager skipped invalid generator '%1' (%2).", _type, _variableName]] call forge_server_common_fnc_log; + continue; + }; + + _generators pushBack [_type, _generator]; + } forEach [ + ["attack", "AttackMissionGenerator"], + ["defend", "DefendMissionGenerator"], + ["defuse", "DefuseMissionGenerator"], + ["delivery", "DeliveryMissionGenerator"], + ["destroy", "DestroyMissionGenerator"], + ["hostage", "HostageMissionGenerator"], + ["hvtkill", "HvtMissionGenerator"], + ["hvtcapture", "CaptureHvtMissionGenerator"] + ]; + + _self set ["generators", _generators]; + }], + ["getMissionInterval", compileFinal { + private _interval = _self getOrDefault ["missionInterval", 300]; + if (_interval <= 0) then { _interval = 300; }; + _interval + }], + ["getMaxConcurrentMissions", compileFinal { + private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1]; + if (_maxConcurrent <= 0) then { _maxConcurrent = 1; }; + _maxConcurrent + }], + ["getActiveMissionIds", compileFinal { + keys (_self getOrDefault ["activeMissionRegistry", createHashMap]) + }], + ["getGeneratorByType", compileFinal { + params [["_generatorType", "", [""]]]; + + private _result = createHashMap; + { + if ((_x param [0, "", [""]]) isEqualTo _generatorType) exitWith { + _result = _x param [1, createHashMap, [createHashMap]]; + }; + } forEach (_self getOrDefault ["generators", []]); + _result + }], + ["selectGeneratorType", compileFinal { + private _missionConfig = _self getOrDefault ["missionConfig", configNull]; + private _weightsConfig = _missionConfig >> "MissionWeights"; + private _weighted = []; + private _totalWeight = 0; + + { + private _generatorType = _x param [0, "", [""]]; + private _weight = getNumber (_weightsConfig >> _generatorType); + if (_weight <= 0) then { _weight = 1; }; + + _totalWeight = _totalWeight + _weight; + _weighted pushBack [_generatorType, _totalWeight]; + } forEach (_self getOrDefault ["generators", []]); + + if (_weighted isEqualTo [] || { _totalWeight <= 0 }) exitWith { "" }; + + private _roll = random _totalWeight; + private _selected = (_weighted select 0) param [0, "", [""]]; + { + if (_roll <= (_x param [1, 0, [0]])) exitWith { + _selected = _x param [0, "", [""]]; + }; + } forEach _weighted; + + _selected + }], + ["startRandomMission", compileFinal { + private _generatorType = _self call ["selectGeneratorType", []]; + if (_generatorType isEqualTo "") exitWith { "" }; + + private _generator = _self call ["getGeneratorByType", [_generatorType]]; + if (_generator isEqualTo createHashMap) exitWith { "" }; + + private _taskID = _generator call ["startMission", [_self]]; + if (_taskID isEqualTo "") exitWith { + ["WARNING", format ["Mission manager failed to start '%1' generated mission.", _generatorType]] call forge_server_common_fnc_log; + "" + }; + + ["INFO", format ["Mission manager started %1 generated mission %2.", _generatorType, _taskID]] call forge_server_common_fnc_log; + _taskID + }], + ["completeMission", compileFinal { + params [["_taskID", "", [""]]]; + if (_taskID isEqualTo "") exitWith { false }; + + private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap]; + private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap]; + private _generatorType = _missionRecord getOrDefault ["generatorType", ""]; + private _generator = _self call ["getGeneratorByType", [_generatorType]]; + + if !(_generator isEqualTo createHashMap) then { + _generator call ["completeMission", [_self, _taskID]]; + } else { + _activeMissionRegistry deleteAt _taskID; + _self set ["activeMissionRegistry", _activeMissionRegistry]; + }; + + true + }] +]; + +MissionManager = createHashMapObject [MissionManagerBaseClass]; +publicVariable "MissionManager"; + +["INFO", format [ + "Mission manager initialized. Interval=%1s, MaxConcurrent=%2", + MissionManager call ["getMissionInterval", []], + MissionManager call ["getMaxConcurrentMissions", []] +]] call forge_server_common_fnc_log; + +private _missionManagerLoop = { + if (isNil "forge_server_task_TaskStore") exitWith { + ["WARNING", "Mission manager waiting for forge_server_task_TaskStore."] call forge_server_common_fnc_log; + }; + + // Drop completed/failed task ids so concurrency is based on live missions. + { + private _status = forge_server_task_TaskStore call ["getTaskStatus", [_x]]; + private _hasCatalogEntry = forge_server_task_TaskStore call ["hasTaskCatalogEntry", [_x]]; + if (_status in ["succeeded", "failed"] || { _status isEqualTo "" && { !_hasCatalogEntry } }) then { + MissionManager call ["completeMission", [_x]]; + forge_server_task_TaskStore call ["clearTaskStatus", [_x]]; + }; + } forEach (MissionManager call ["getActiveMissionIds", []]); + + if (count (MissionManager call ["getActiveMissionIds", []]) >= (MissionManager call ["getMaxConcurrentMissions", []])) exitWith {}; + + // The PFH interval is the wake-up cadence; this timestamp prevents a task + // from being created too quickly if the interval is changed at runtime. + private _now = diag_tickTime; + private _interval = MissionManager call ["getMissionInterval", []]; + private _lastMissionGenerationAt = MissionManager getOrDefault ["lastMissionGenerationAt", -1e10]; + if ((_now - _lastMissionGenerationAt) < _interval) exitWith {}; + + MissionManager set ["lastMissionGenerationAt", _now]; + + private _taskID = MissionManager call ["startRandomMission", []]; + if (_taskID isEqualTo "") then { + ["WARNING", "Mission manager did not start a generated mission this cycle."] call forge_server_common_fnc_log; + }; +}; + +[ + { + params ["_args"]; + _args params ["_loop"]; + [] call _loop; + }, + MissionManager call ["getMissionInterval", []], + [_missionManagerLoop] +] call CBA_fnc_addPerFrameHandler; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_persistentCadMissionManager.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_persistentCadMissionManager.sqf new file mode 100644 index 0000000..c7b67ce --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_persistentCadMissionManager.sqf @@ -0,0 +1,149 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Persistent CAD-oriented mission dispatcher. Creates CAD-assignable Forge + * tasks by randomly selecting a mission generator and running startMission. + * + * Arguments: + * None + * + * Return Value: + * N/A + * + * Public: No + */ + +private _makeGenerator = { + params ["_generatorType"]; + + switch (_generatorType) do { + case "attack": { [] call forge_pmc_fnc_attackMissionGenerator }; + case "defend": { [] call forge_pmc_fnc_defendMissionGenerator }; + case "defuse": { [] call forge_pmc_fnc_defuseMissionGenerator }; + case "delivery": { [] call forge_pmc_fnc_deliveryMissionGenerator }; + case "destroy": { [] call forge_pmc_fnc_destroyMissionGenerator }; + case "hostage": { [] call forge_pmc_fnc_hostageMissionGenerator }; + case "hvt": { [] call forge_pmc_fnc_hvtMissionGenerator }; + case "hvtkill": { [] call forge_pmc_fnc_hvtMissionGenerator }; + case "hvtcapture": { [] call forge_pmc_fnc_captureHvtMissionGenerator }; + default { createHashMap }; + }; +}; + +// Ensure generator symbols exist by compiling generator scripts if needed. +private _ensureGeneratorsLoaded = { + // compileFinal preprocess keeps it in mission namespace + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_attackMissionGenerator.sqf"; + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_defendMissionGenerator.sqf"; + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_defuseMissionGenerator.sqf"; + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_deliveryMissionGenerator.sqf"; + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_destroyMissionGenerator.sqf"; + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_hostageMissionGenerator.sqf"; + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_hvtMissionGenerator.sqf"; + [] call compileFinal preprocessFileLineNumbers "functions\\missionGenerators\\fn_captureHvtMissionGenerator.sqf"; +}; + +// Load generator scripts once. +call _ensureGeneratorsLoaded; + + + +private _getGeneratorTypes = { + ["attack", "defend", "defuse", "delivery", "destroy", "hostage", "hvtkill", "hvtcapture"] +}; + +PersistentCadMissionManagerBaseClass = compileFinal createHashMapFromArray [ + ["#type", "PersistentCadMissionManagerBaseClass"], + + ["#create", compileFinal { + params ["_self"]; + private _missionConfig = missionConfigFile >> "CfgMissions"; + _self set ["missionConfig", _missionConfig]; + _self set ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")]; + _self set ["missionInterval", getNumber (_missionConfig >> "missionInterval")]; + _self set ["activeMissionRegistry", createHashMap]; + }], + + ["getMissionInterval", compileFinal { + private _interval = _self getOrDefault ["missionInterval", 300]; + if (_interval <= 0) then { _interval = 300; }; + _interval + }], + + ["getMaxConcurrentMissions", compileFinal { + private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1]; + if (_maxConcurrent <= 0) then { _maxConcurrent = 1; }; + _maxConcurrent + }], + + ["getActiveMissionIds", compileFinal { + keys (_self getOrDefault ["activeMissionRegistry", createHashMap]) + }], + + ["_pickGeneratorType", compileFinal { + selectRandom (call _getGeneratorTypes) + }], + + ["startRandomGeneratorMission", compileFinal { + private _generatorType = _self call ["_pickGeneratorType", []]; + private _generator = [_generatorType] call _makeGenerator; + if (_generator isEqualTo createHashMap) exitWith { "" }; + + // Generators themselves call forge_server_task_fnc_startTask. + private _taskID = _generator call ["startMission", [_self]]; + if (_taskID isEqualTo "") exitWith { "" }; + + _taskID + }], + + ["completeMission", compileFinal { + params ["_taskID"]; + if (_taskID isEqualTo "") exitWith { false }; + + private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap]; + private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap]; + private _generatorType = _missionRecord getOrDefault ["generatorType", ""]; + private _generator = [_generatorType] call _makeGenerator; + + if !(_generator isEqualTo createHashMap) then { + _generator call ["completeMission", [_self, _taskID]]; + } else { + _activeMissionRegistry deleteAt _taskID; + _self set ["activeMissionRegistry", _activeMissionRegistry]; + }; + + true + }] +]; + +PersistentCadMissionManager = createHashMapObject [PersistentCadMissionManagerBaseClass]; +PublicVariable "PersistentCadMissionManager"; + +private _loop = { + // Cleanup completed/failed missions + { + private _taskID = _x; + private _status = forge_server_task_TaskStore call ["getTaskStatus", [_taskID]]; + private _hasCatalogEntry = forge_server_task_TaskStore call ["hasTaskCatalogEntry", [_taskID]]; + if (_status in ["succeeded", "failed"] || { _status isEqualTo "" && { !_hasCatalogEntry } }) then { + PersistentCadMissionManager call ["completeMission", [_taskID]]; + forge_server_task_TaskStore call ["clearTaskStatus", [_taskID]]; + }; + } forEach (PersistentCadMissionManager call ["getActiveMissionIds", []]); + + if ((count (PersistentCadMissionManager call ["getActiveMissionIds", []])) >= (PersistentCadMissionManager call ["getMaxConcurrentMissions", []])) exitWith {}; + + private _taskID = PersistentCadMissionManager call ["startRandomGeneratorMission", []]; + if (_taskID isEqualTo "") exitWith { + ["WARNING", "Persistent CAD mission dispatcher failed to start a random mission."] call forge_server_common_fnc_log; + }; + + ["INFO", format ["Persistent CAD mission dispatcher started task %1", _taskID]] call forge_server_common_fnc_log; +}; + +if (isServer) then { + [ + { call _loop; }, + PersistentCadMissionManager call ["getMissionInterval", []], + [] + ] call cba_fnc_addPerFrameHandler; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_updateEnemyCountFromActivePlayers.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_updateEnemyCountFromActivePlayers.sqf new file mode 100644 index 0000000..307df00 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionManager/fn_updateEnemyCountFromActivePlayers.sqf @@ -0,0 +1,55 @@ +/* + * 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 + * + * 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 forge_server_common_fnc_log; + +_multiplier diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/README.md b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/README.md new file mode 100644 index 0000000..e80fefc --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/README.md @@ -0,0 +1,20 @@ +# Mission Setup Functions + +Mission setup functions own startup configuration for `forge_pmc_simulator.Tanoa`. + +## Registered Functions +- `forge_pmc_fnc_openMissionSetupUI` opens the browser-control startup UI. +- `forge_pmc_fnc_handleMissionSetupUIEvents` handles UI events from `A3API.SendAlert`. +- `forge_pmc_fnc_setupMenu_applySettings` applies UI overrides or mission parameter defaults into `forge_pmc_missionSettings`. + +## Startup Flow +The client opens the setup UI from `initPlayerLocal.sqf` when `forge_pmc_missionSettingsApplied` is not set. + +Selecting **Start Mission** sends UI values to the server and applies them. Selecting **Cancel** applies the existing Arma mission params/defaults immediately. + +## Shared State +- `forge_pmc_missionSettings`: HashMap of applied settings. +- `forge_pmc_missionSettingsApplied`: Boolean gate used by the mission manager. +- `ENEMY_FACTION_STR` and `ENEMY_SIDE`: selected faction and resolved Arma side. + +Reusable faction and setting lookup helpers live in `functions\helpers`. diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_handleMissionSetupUIEvents.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_handleMissionSetupUIEvents.sqf new file mode 100644 index 0000000..cf13de6 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_handleMissionSetupUIEvents.sqf @@ -0,0 +1,102 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Handles JSON events sent by the PMC mission setup browser UI through + * A3API.SendAlert. + * + * Arguments: + * 0: Browser control + * 1: Whether the event came from a confirm dialog + * 2: JSON event payload + * + * Return Value: + * Event handled + * + * Public: No + */ + +params [ + ["_control", controlNull, [controlNull]], + ["_isConfirmDialog", false, [false]], + ["_message", "", [""]] +]; + +if (_message isEqualTo "") exitWith { false }; + +private _alert = fromJSON _message; +if !(_alert isEqualType createHashMap) exitWith { false }; + +private _event = _alert getOrDefault ["event", ""]; +private _data = _alert getOrDefault ["data", createHashMap]; + +// Keep all browser responses on the existing framework bridge so the mission +// setup UI behaves like other HTML-backed Forge displays. +private _send = { + params ["_eventName", "_payload"]; + + if (isNull _control) exitWith { false }; + + private _json = toJSON createHashMapFromArray [ + ["event", _eventName], + ["data", _payload] + ]; + _control ctrlWebBrowserAction ["ExecJS", format ["MissionSetupBridge.receive(%1)", _json]]; + true +}; + +switch (_event) do { + case "missionSetup::ready": { + private _missionConfig = missionConfigFile >> "CfgMissions"; + private _attackConfig = _missionConfig >> "MissionTypes" >> "Attack"; + private _factions = []; + + { + _x params ["_faction", "_display", "_value"]; + _factions pushBack createHashMapFromArray [ + ["faction", _faction], + ["display", _display], + ["value", _value] + ]; + } forEach ([] call forge_pmc_fnc_getEnemyFactionOptions); + + private _payload = createHashMapFromArray [ + ["factions", _factions], + ["settings", createHashMapFromArray [ + ["enemyFaction", "IND_G_F"], + ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")], + ["missionInterval", getNumber (_missionConfig >> "missionInterval")], + ["moneyMin", (getArray (_attackConfig >> "Rewards" >> "money")) param [0, 25000]], + ["moneyMax", (getArray (_attackConfig >> "Rewards" >> "money")) param [1, 60000]], + ["reputationMin", (getArray (_attackConfig >> "Rewards" >> "reputation")) param [0, 6]], + ["reputationMax", (getArray (_attackConfig >> "Rewards" >> "reputation")) param [1, 14]], + ["penaltyMin", (getArray (_attackConfig >> "penalty")) param [0, -8]], + ["penaltyMax", (getArray (_attackConfig >> "penalty")) param [1, -3]], + ["timeLimitMin", (getArray (_attackConfig >> "timeLimit")) param [0, 900]], + ["timeLimitMax", (getArray (_attackConfig >> "timeLimit")) param [1, 1800]] + ]] + ]; + + ["missionSetup::hydrate", _payload] call _send; + }; + case "missionSetup::apply": { + if !(_data isEqualType createHashMap) exitWith { + ["missionSetup::error", createHashMapFromArray [["message", "Invalid mission setup payload."]]] call _send; + }; + + [_data] remoteExecCall ["forge_pmc_fnc_setupMenu_applySettings", 2]; + closeDialog 1; + }; + case "missionSetup::cancel": { + // Cancelling still applies mission params/defaults so postInit startup + // can continue without waiting for explicit UI confirmation. + [] remoteExecCall ["forge_pmc_fnc_setupMenu_applySettings", 2]; + closeDialog 1; + }; + case "missionSetup::close": { + closeDialog 1; + }; + default { + ["missionSetup::error", createHashMapFromArray [["message", format ["Unhandled setup event: %1", _event]]]] call _send; + }; +}; + +true diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_openMissionSetupUI.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_openMissionSetupUI.sqf new file mode 100644 index 0000000..e5714d0 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_openMissionSetupUI.sqf @@ -0,0 +1,54 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Opens the PMC simulator startup setup UI and wires browser events into + * forge_pmc_fnc_handleMissionSetupUIEvents. + * + * Arguments: + * None + * + * Return Value: + * UI opened + * + * Public: No + */ + +if (!hasInterface) exitWith { false }; +if (missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) exitWith { false }; + +private _ceoUnit = missionNamespace getVariable ["ceo", objNull]; +private _isCeoSlot = + (!isNull _ceoUnit && { player isEqualTo _ceoUnit }) || + { toLowerANSI (vehicleVarName player) isEqualTo "ceo" }; + +if (!_isCeoSlot) exitWith { false }; + +private _display = createDialog ["RscPmcMissionSetup", true]; +if (isNull _display) exitWith { false }; + +private _control = _display displayCtrl 92011; +if (isNull _control) exitWith { + closeDialog 1; + false +}; + +_control ctrlAddEventHandler ["JSDialog", { + params ["_control", "_isConfirmDialog", "_message"]; + [_control, _isConfirmDialog, _message] call forge_pmc_fnc_handleMissionSetupUIEvents; +}]; + +_control ctrlWebBrowserAction ["LoadFile", "ui\_site\index.html"]; + +[] spawn { + waitUntil { + sleep 0.25; + isNull (uiNamespace getVariable ["RscPmcMissionSetup", displayNull]) || + { missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false] } + }; + + private _display = uiNamespace getVariable ["RscPmcMissionSetup", displayNull]; + if (!isNull _display) then { + closeDialog 1; + }; +}; + +true diff --git a/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_setupMenu_applySettings.sqf b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_setupMenu_applySettings.sqf new file mode 100644 index 0000000..cdef74a --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/functions/missionSetup/fn_setupMenu_applySettings.sqf @@ -0,0 +1,125 @@ +/* + * Author: IDSolutions, Blackbox AI, MrPākehā + * Applies startup UI overrides or Arma mission parameter defaults into + * missionNamespace for server-side generators and managers. + * + * Arguments: + * 0: UI override settings (Default: createHashMap) + * + * Return Value: + * Settings applied + * + * Public: No + */ + +if (!isServer) exitWith {}; + +params [ + ["_overrides", createHashMap, [createHashMap]] +]; + +private _settings = createHashMap; + +// Always allow menu-style overrides for this mission. +_settings set ["useMenuSettings", true]; + +// Mission parameter IDs are exported as variables in the missionNamespace. +// We defensively read them via missionNamespace so the function also works +// when called before mission parameter evaluation. + +private _paramOrDefault = { + params ["_varName", "_default"]; + + if (_varName in _overrides) exitWith { + _overrides getOrDefault [_varName, _default] + }; + + private _v = missionNamespace getVariable [_varName, _default]; + _v +}; + +// Numeric settings may come from either the browser payload or mission params. +private _maxConcurrent = ["maxConcurrentMissions", 3] call _paramOrDefault; +private _interval = ["missionInterval", 300] call _paramOrDefault; + +private _moneyMin = ["moneyMin", 25000] call _paramOrDefault; +private _moneyMax = ["moneyMax", 60000] call _paramOrDefault; + +private _repMin = ["reputationMin", 6] call _paramOrDefault; +private _repMax = ["reputationMax", 14] call _paramOrDefault; + +private _penMin = ["penaltyMin", -8] call _paramOrDefault; +private _penMax = ["penaltyMax", -3] call _paramOrDefault; + +private _timeMin = ["timeLimitMin", 900] call _paramOrDefault; +private _timeMax = ["timeLimitMax", 1800] call _paramOrDefault; + +// Enemy faction selection falls back to Params::enemyFaction when the setup UI +// is closed without pressing Start Mission. +private _enemyFactionParam = ["enemyFaction", 7] call BIS_fnc_getParamValue; +private _enemyFaction = _overrides getOrDefault ["enemyFaction", ""]; +if (_enemyFaction isEqualTo "") then { + if (_enemyFactionParam isEqualTo -1) then { + _enemyFactionParam = 7; + }; + _enemyFaction = [_enemyFactionParam, "IND_G_F"] call forge_pmc_fnc_resolveEnemyFactionParam; +} else { + _enemyFactionParam = _enemyFaction; +}; + +// Keep a copy of the resolved faction classname for spawning/gear systems. +missionNamespace setVariable ["ENEMY_FACTION_STR", _enemyFaction]; + +// Normalize ranges so generator code can safely assume min <= max. +_moneyMin = _moneyMin max 0; +_moneyMax = _moneyMax max _moneyMin; + +_repMin = _repMin max -100000; +_repMax = _repMax max _repMin; + +_penMin = _penMin min 0; +_penMax = _penMax min 0; +if (_penMax < _penMin) then { _penMax = _penMin; }; + +_timeMin = _timeMin max 1; +_timeMax = _timeMax max _timeMin; + +// Publish the normalized settings map used by mission generators. +_settings set ["maxConcurrentMissions", _maxConcurrent]; +_settings set ["missionInterval", _interval]; + +_settings set ["moneyMin", _moneyMin]; +_settings set ["moneyMax", _moneyMax]; + +_settings set ["reputationMin", _repMin]; +_settings set ["reputationMax", _repMax]; + +_settings set ["penaltyMin", _penMin]; +_settings set ["penaltyMax", _penMax]; + +_settings set ["timeLimitMin", _timeMin]; +_settings set ["timeLimitMax", _timeMax]; + +_settings set ["enemyFaction", _enemyFaction]; + +missionNamespace setVariable ["forge_pmc_missionSettings", _settings, true]; +missionNamespace setVariable ["forge_pmc_missionSettingsApplied", true, true]; + +// Export a simple readiness flag for any scripts that expect settings to exist. +missionNamespace setVariable ["forge_pmc_missionSettings_getterReady", true, true]; + +// Convert faction classname to Arma side. +private _side = [_enemyFaction, east] call forge_pmc_fnc_getEnemyFactionSide; + +diag_log format [ + "[FORGE:MissionSettings] Enemy faction param=%1 resolved=%2 side=%3", + _enemyFactionParam, + _enemyFaction, + _side +]; + +ENEMY_SIDE = _side; +missionNamespace setVariable ["ENEMY_FACTION_STR", _enemyFaction, true]; +publicVariable "ENEMY_SIDE"; + +true; diff --git a/arma/forge_pmc_simulator.Tanoa/guides/ACTOR_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/ACTOR_USAGE_GUIDE.md new file mode 100644 index 0000000..5d55248 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/ACTOR_USAGE_GUIDE.md @@ -0,0 +1,143 @@ +# Actor Usage Guide + +The actor module stores persistent player character data: identity, loadout, +position, direction, stance, contact fields, state, holster status, rank, and +organization. + +## Storage Model + +Actor data is persisted through SurrealDB by the server extension. + +```json +{ + "uid": "76561198000000000", + "name": "Player Name", + "loadout": {}, + "position": [1234.5, 6789.0, 0.0], + "direction": 90.0, + "stance": "STAND", + "email": "0160000000@spearnet.mil", + "phone_number": "0160000000", + "state": "HEALTHY", + "holster": true, + "rank": null, + "organization": "default" +} +``` + +Rules validated by the Rust service: + +- `uid` is authoritative from the command argument and must be a 17-digit Steam + UID. +- `name` is optional, but cannot be empty when set and cannot exceed 50 + characters. +- `position` must be three finite numbers when set. +- `direction` must be in the `0.0 <= direction < 360.0` range. +- `email` must contain `@` and end with `.mil` when set. +- `phone_number` must start with `0160` and be 10 digits when set. +- Empty `phone_number`, `email`, or `organization` fields are filled on create. + +## Commands + +All commands are called on the `actor` group. + +| Command | Arguments | Returns | +| --- | --- | --- | +| `actor:get` | `uid` | Actor JSON. If no actor exists, returns a default actor but does not persist it. | +| `actor:create` | `uid`, `actor_json` | Persisted actor JSON. | +| `actor:update` | `uid`, `patch_json` | Updated actor JSON. | +| `actor:exists` | `uid` | `true` or `false`. | +| `actor:delete` | `uid` | `OK`. | + +## Create an Actor + +The `uid` field in the JSON is overwritten with the command UID. + +```sqf +private _actor = createHashMapFromArray [ + ["uid", getPlayerUID player], + ["name", name player], + ["loadout", getUnitLoadout player], + ["position", getPosATL player], + ["direction", getDir player], + ["stance", stance player], + ["email", ""], + ["phone_number", ""], + ["state", "HEALTHY"], + ["holster", true], + ["organization", "default"] +]; + +private _result = "forge_server" callExtension ["actor:create", [ + getPlayerUID player, + toJSON _actor +]]; +``` + +## New Player Bootstrap + +The server actor store treats a player with no persisted actor as a new player. +After `actor:create` succeeds, the actor store runs onboarding once for that UID: + +- Initializes the player's phone state. +- Sends a Field Commander email from `field_commander` with the `Job Orientation` + subject and the generated phone number and email address. +- Sends two Field Commander text messages with the first-day instructions. +- Initializes the player's bank account if needed and adds `$2,000` to the bank + balance. + +This bootstrap is tied to persistent actor creation, not hot-state hydration, so +returning players and repaired partial actor records do not receive the welcome +messages or starting money again. + +## Update an Actor + +`actor:update` accepts a JSON object containing only fields to change. + +```sqf +private _patch = createHashMapFromArray [ + ["position", getPosATL player], + ["direction", getDir player], + ["stance", stance player], + ["loadout", getUnitLoadout player] +]; + +private _result = "forge_server" callExtension ["actor:update", [ + getPlayerUID player, + toJSON _patch +]]; +``` + +Supported patch fields are `name`, `position`, `direction`, `stance`, `email`, +`phone_number`, `state`, `holster`, `rank`, `organization`, and `loadout`. +`uid` is ignored. + +## Hot State + +The `actor:hot:*` commands keep a runtime copy of actor data and write it back +only when `actor:hot:save` runs. + +| Command | Arguments | Returns | +| --- | --- | --- | +| `actor:hot:init` | `uid` | Actor JSON from durable storage. | +| `actor:hot:get` | `uid` | Actor JSON. | +| `actor:hot:keys` | none | JSON array of hot actor UIDs. | +| `actor:hot:override` | `uid`, `actor_json` | Actor JSON. | +| `actor:hot:save` | `uid` | Current hot actor JSON and async durable save. | +| `actor:hot:remove` | `uid` | `OK`. | + +Use hot state for frequently updated session data such as position and loadout. +Use durable commands for account creation and administrative changes. + +## Error Handling + +```sqf +private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]]; +private _payload = _result select 0; + +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Actor error: %1", _payload]; +}; + +private _actor = fromJSON _payload; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/AI_UI_Prompt_Guide.md b/arma/forge_pmc_simulator.Tanoa/guides/AI_UI_Prompt_Guide.md new file mode 100644 index 0000000..39db23a --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/AI_UI_Prompt_Guide.md @@ -0,0 +1,570 @@ +# AI Prompt: Creating Arma 3 UI Windows (Dialogs) - Complete Guide + +This document provides a comprehensive prompt for creating UI windows (dialogs) for Arma 3 missions. + +--- + +## Table of Contents + +1. [Core Concepts](#core-concepts) +2. [Dialog File Structure](#dialog-file-structure) +3. [Base Control Classes](#base-control-classes) +4. [Opening Dialogs via Scripts](#opening-dialogs-via-scripts) +5. [Interacting with Controls](#interacting-with-controls) +6. [Complete Example: Creating a New Store Dialog](#complete-example-creating-a-new-store-dialog) +7. [Integration Steps](#integration-steps) + +--- + +## Core Concepts + +### What Are Dialogs? + +Dialogs in Arma 3 are UI windows defined in `*.hpp` files using a class-based syntax. They are defined using config classes that inherit from base control classes. + +### Key Properties + +- **idd**: Dialog ID (must be unique, e.g., 420, 6969, 9290) +- **movingEnable**: Allows dragging the window (true/false) +- **enableSimulation**: Allows simulation when open (true/false) + +### Position System + +Arma 3 uses a screen-relative coordinate system: +- `safezoneW` = safe zone width +- `safezoneH` = safe zone height +- `safezoneX` = safe zone X offset +- `safezoneY` = safe zone Y offset + +Formula: `x = (position * safezoneW) + safezoneX` +Formula: `y = (position * safezoneH) + safezoneY` + +Example: `x = 0.5 * safezoneW + safezoneX` places control at 50% screen width + +--- + +## Dialog File Structure + +### Basic Dialog Template + +```cpp +/* + +Dialog Name - Version by Author +[License/Credits] + +################################## LET US BEGIN #################################### */ + +class Dialog_ClassName { + idd = UNIQUE_ID; + movingEnable = true; + enableSimulation = true; + + class Controls { + + //////////////////////////////////////////////////////// + // GUI EDITOR OUTPUT START + //////////////////////////////////////////////////////// + + // Add your controls here + + //////////////////////////////////////////////////////// + // GUI EDITOR OUTPUT END + //////////////////////////////////////////////////////// + }; +}; +``` + +--- + +## Base Control Classes + +### 1. RscText (Static Text) + +```cpp +class MyText: RscText { + idc = 1000; + text = "Label Text"; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.1 * safezoneW; + h = 0.033 * safezoneH; +}; +``` + +### 2. RscStructuredText (Rich Text with HTML-like formatting) + +```cpp +class MyStructuredText: RscStructuredText { + idc = 1100; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.1 * safezoneW; + h = 0.033 * safezoneH; + colorBackground[] = {0,0,0,0}; // Optional background +}; +``` + +Set text dynamically: +```cpp +_ChildControl ctrlSetStructuredText parseText format ["Value: %1", myValue]; +``` + +### 3. RscPicture + +```cpp +class MyPicture: RscPicture { + idc = 1200; + text = "images\myimage.paa"; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.2 * safezoneW; + h = 0.15 * safezoneH; +}; +``` + +### 4. RscButton + +```cpp +class MyButton: RscButton { + onButtonClick = "[] call myFunction;"; + idc = 1600; + text = "Click Me"; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.1 * safezoneW; + h = 0.033 * safezoneH; + tooltip = "Tooltip text"; +}; +``` + +### 5. RscEdit (Input Field) + +```cpp +class MyEdit: RscEdit { + idc = 1400; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.1 * safezoneW; + h = 0.033 * safezoneH; + tooltip = "Enter value here"; +}; +``` + +Get value: +```cpp +_value = ctrlText findDisplay DIALOG_ID displayCtrl CONTROL_ID; +``` + +### 6. RscListbox + +```cpp +class MyListbox: RscListBox { + onLBDblClick = "_this spawn myFunction;"; + idc = 1500; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.3 * safezoneW; + h = 0.4 * safezoneH; +}; +``` + +Populate dynamically: +```cpp +lbClear CONTROL_ID; +{ + lbAdd [CONTROL_ID, _x select 0]; +} forEach myArray; +``` + +Get selection: +```cpp +_selectedIndex = lbCurSel CONTROL_ID; +_selectedValue = myArray select _selectedIndex; +``` + +### 7. RscCombo (Dropdown) + +```cpp +class MyCombo: RscCombo { + onLBSelChanged = "_this spawn myFunction;"; + idc = 2100; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.15 * safezoneW; + h = 0.033 * safezoneH; +}; +``` + +### 8. RscFrame (Window Border/Title) + +```cpp +class MyFrame: RscFrame { + Moving = 1; // Allow moving with this frame + idc = 1800; + text = "Window Title"; + x = 0.3 * safezoneW + safezoneX; + y = 0.3 * safezoneH + safezoneY; + w = 0.4 * safezoneW; + h = 0.4 * safezoneH; +}; +``` + +### 9. RscClrButton (Colored/Image Button) + +```cpp +class MyImageButton: RscClrButton { + onButtonClick = "[] call myFunction;"; + idc = 2000; + x = 0.5 * safezoneW + safezoneX; + y = 0.5 * safezoneH + safezoneY; + w = 0.2 * safezoneW; + h = 0.1 * safezoneH; + tooltip = "Button tooltip"; +}; +``` + +--- + +## Opening Dialogs via Scripts + +### Method 1: Using CreateDialog (Simple) + +```cpp +// In a script file (.sqf) +_handle = CreateDialog "Dialog_ClassName"; +``` + +### Method 2: Using execVM + +```cpp +// In a script file (.sqf) +execVM "scripts\myDialogScript.sqf"; +``` + +The script then calls CreateDialog. + +### Example: Triggering from an Action + +```cpp +// Add to object's init line +this addAction ["Open Menu", "execVM 'scripts\myDialogScript.sqf'"]; +``` + +--- + +## Interacting with Controls + +### Finding the Display + +```cpp +disableSerialization; +_disp = findDisplay DIALOG_ID; +``` + +### Getting a Control + +```cpp +_ctrl = _disp displayCtrl CONTROL_ID; +``` + +### Setting Text + +```cpp +_ctrl ctrlSetText "New Text"; +``` + +### Getting Text + +```cpp +_value = ctrlText CONTROL_ID; +``` + +### Setting Structured Text + +```cpp +_ctrl ctrlSetStructuredText parseText format ["Value: %1", myValue]; +``` + +### Setting Focus + +```cpp +ctrlSetFocus _ctrl; +``` + +### Hiding/Showing Controls + +```cpp +_ctrl ctrlShow false; // Hide +_ctrl ctrlShow true; // Show +``` + +### Enabling/Disabling Controls + +```cpp +_ctrl ctrlEnable false; // Disable +_ctrl ctrlEnable true; // Enable +``` + +--- + +## Complete Example: Creating a New Store Dialog + +### Step 1: Create the Dialog Definition File + +File: `dialogs\MyNewStore.hpp` + +```cpp +/* + +My New Store GUI V 1.0 by [Your Name] + +License: +[Your License] + +################################## LET US BEGIN #################################### */ + +class A3M_MyNewStore { + idd = 5000; // Use a unique ID! + movingEnable = true; + enableSimulation = true; + + class Controls { + + //////////////////////////////////////////////////////// + // GUI EDITOR OUTPUT START + //////////////////////////////////////////////////////// + + class MyStore_Frame: RscFrame { + Moving = 1; + idc = 1800; + text = "My New Store"; + x = 0.3 * safezoneW + safezoneX; + y = 0.3 * safezoneH + safezoneY; + w = 0.4 * safezoneW; + h = 0.4 * safezoneH; + }; + + class MyStore_ExitButton: RscButton { + onButtonClick = "closeDialog 0;"; + idc = 1600; + text = "Exit"; + x = 0.65 * safezoneW + safezoneX; + y = 0.65 * safezoneH + safezoneY; + w = 0.04 * safezoneW; + h = 0.033 * safezoneH; + }; + + class MyStore_CategoryTitle: RscText { + idc = 1000; + text = "Categories:"; + x = 0.32 * safezoneW + safezoneX; + y = 0.32 * safezoneH + safezoneY; + w = 0.08 * safezoneW; + h = 0.033 * safezoneH; + }; + + class MyStore_Btn_Items: RscButton { + onButtonClick = "[] call MyStore_fnc_loadItems;"; + idc = 1610; + text = "Browse Items"; + x = 0.32 * safezoneW + safezoneX; + y = 0.36 * safezoneH + safezoneY; + w = 0.1 * safezoneW; + h = 0.033 * safezoneH; + }; + + class MyStore_ItemList: RscListBox { + onLBDblClick = "_this spawn MyStore_fnc_handleSelect;"; + idc = 1500; + x = 0.32 * safezoneW + safezoneX; + y = 0.41 * safezoneH + safezoneY; + w = 0.36 * safezoneW; + h = 0.2 * safezoneH; + }; + + class MyStore_InfoTitle: RscText { + idc = 1001; + text = "Information:"; + x = 0.5 * safezoneW + safezoneX; + y = 0.32 * safezoneH + safezoneY; + w = 0.1 * safezoneW; + h = 0.033 * safezoneH; + }; + + class MyStore_InfoDisplay: RscStructuredText { + idc = 1100; + x = 0.5 * safezoneW + safezoneX; + y = 0.36 * safezoneH + safezoneY; + w = 0.18 * safezoneW; + h = 0.1 * safezoneH; + colorBackground[] = {0,0,0,0.3}; + }; + + class MyStore_Btn_Purchase: RscButton { + onButtonClick = "[] call MyStore_fnc_purchaseItem;"; + idc = 1620; + text = "Purchase"; + x = 0.6 * safezoneW + safezoneX; + y = 0.65 * safezoneW + safezoneW; + w = 0.08 * safezoneW; + h = 0.033 * safezoneH; + }; + + class MyStore_BalanceTitle: RscText { + idc = 1002; + text = "Balance:"; + x = 0.32 * safezoneW + safezoneX; + y = 0.65 * safezoneH + safezoneY; + w = 0.06 * safezoneW; + h = 0.033 * safezoneH; + }; + + class MyStore_BalanceDisplay: RscStructuredText { + idc = 1101; + x = 0.38 * safezoneW + safezoneX; + y = 0.65 * safezoneH + safezoneY; + w = 0.1 * safezoneW; + h = 0.033 * safezoneH; + }; + + //////////////////////////////////////////////////////// + // GUI EDITOR OUTPUT END + //////////////////////////////////////////////////////// + }; +}; +``` + +### Step 2: Create the Handler Script + +File: `scripts\MyNewStore.sqf` + +```cpp +/* + +My New Store Script by [Your Name] + +################################## LET US BEGIN #################################### */ + +// Open the dialog +_handle = CreateDialog "A3M_MyNewStore"; + +// Define available items +myStoreItems = [ + ["Item Name 1", 100], + ["Item Name 2", 200], + ["Item Name 3", 300] +]; + +// Update balance display +MyStore_UpdateBalance = { + disableSerialization; + _disp = findDisplay 5000; + if (str(_disp) != "no display") then { + _ctrl = _disp displayCtrl 1101; + _balance = player getVariable "myMoney"; + _ctrl ctrlSetStructuredText parseText format ["$%1", _balance]; + }; +}; +[] call MyStore_UpdateBalance; + +// Load items into listbox +MyStore_fnc_loadItems = { + lbClear 1500; + { + lbAdd [1500, format ["%1 - $%2", _x select 0, _x select 1]]; + } forEach myStoreItems; +}; + +// Handle selection +MyStore_fnc_handleSelect = { + _index = _this select 1; + _item = myStoreItems select _index; + _disp = findDisplay 5000; + _ctrl = _disp displayCtrl 1100; + _ctrl ctrlSetStructuredText parseText format [ + "Selected: %1
Price: $%2", + _item select 0, + _item select 1 + ]; +}; + +// Purchase item +MyStore_fnc_purchaseItem = { + _index = lbCurSel 1500; + if (_index == -1) exitWith { hint "Select an item first!"; }; + + _item = myStoreItems select _index; + _price = _item select 1; + _money = player getVariable "myMoney"; + + if (_money < _price) exitWith { hint "Not enough money!"; }; + + player setVariable ["myMoney", _money - _price]; + hint format ["Purchased %1 for $%2!", _item select 0, _price]; + + [] call MyStore_UpdateBalance; +}; +``` + +### Step 3: Add to description.ext + +```cpp +#include "dialogs\MyNewStore.hpp" +``` + +### Step 4: Add Open Action (Optional) + +Add to an object's init: +```cpp +this addAction ["Open Store", "execVM 'scripts\MyNewStore.sqf'"]; +``` + +--- + +## Integration Steps + +### 1. Create Dialog File (.hpp) + +Create the dialog class definition in the `dialogs/` folder. + +### 2. Include in description.ext + +Add `#include "dialogs\YourDialog.hpp"` to the description.ext file. + +### 3. Register Controls Used + +If using custom control IDs, ensure they don't conflict with existing ones. Common ranges: +- 1000-1099: Text/Static controls +- 1100-1199: Structured text +- 1200-1299: Pictures +- 1400-1499: Edit fields +- 1500-1599: Listboxes +- 1600-1699: Buttons +- 1800-1899: Frames +- 2000-2999: Custom controls + +### 4. Create Handler Script + +Create an `.sqf` script file in the `scripts/` folder to handle the dialog logic. + +### 5. Test Thoroughly + +- Test opening/closing the dialog +- Test all buttons and interactions +- Test list population +- Test form validation +- Test error messages + +--- + +## Best Practices + +1. **Unique IDD**: Always use a unique dialog ID (idd) +2. **Organized Layout**: Use a consistent grid layout +3. **Clear Labels**: Always label information displays +4. **Exit Button**: Always include an exit/close button +5. **Form Validation**: Validate inputs before processing +6. **User Feedback**: Use hints to provide feedback +7. **Responsive Design**: Test on different aspect ratios +8. **Mod Support**: Consider adding mod compatibility checks diff --git a/arma/forge_pmc_simulator.Tanoa/guides/BANK_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/BANK_USAGE_GUIDE.md new file mode 100644 index 0000000..5953c6f --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/BANK_USAGE_GUIDE.md @@ -0,0 +1,190 @@ +# Bank Usage Guide + +The bank module stores player account balances, earnings, PINs, and transaction +strings. The hot-state API also owns the active banking workflows used by the +UI: deposit, withdraw, transfer, checkout charge, PIN validation, and PIN +changes. + +## Storage Model + +Bank data is persisted through SurrealDB by the server extension. + +```json +{ + "uid": "76561198000000000", + "name": "Player Name", + "bank": 1000.0, + "cash": 250.0, + "earnings": 0.0, + "pin": 1234, + "transactions": [] +} +``` + +Rules validated by the Rust service: + +- `uid` is authoritative from the command argument. +- `name` cannot be empty. +- `bank` and `cash` cannot be negative. +- `pin` must be a four-digit number. +- Durable `bank:get` requires an existing bank account. + +## Durable Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `bank:create` | `uid`, `bank_json` | Persisted bank JSON. | +| `bank:get` | `uid` | Bank JSON. | +| `bank:update` | `uid`, `patch_json` | Updated bank JSON. | +| `bank:exists` | `uid` | `true` or `false`. | +| `bank:delete` | `uid` | `OK`. | + +## Create an Account + +The `uid` field in the JSON is overwritten with the command UID. + +```sqf +private _account = createHashMapFromArray [ + ["uid", getPlayerUID player], + ["name", name player], + ["bank", 0], + ["cash", 0], + ["earnings", 0], + ["pin", 1234], + ["transactions", []] +]; + +private _result = "forge_server" callExtension ["bank:create", [ + getPlayerUID player, + toJSON _account +]]; +``` + +## Hot-State Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `bank:hot:init` | `uid` | Bank JSON loaded into hot state. | +| `bank:hot:get` | `uid` | Bank JSON. | +| `bank:hot:override` | `uid`, `bank_json` | Bank JSON. | +| `bank:hot:patch` | `uid`, `patch_json` | `{ account, patch }`. | +| `bank:hot:deposit` | `uid`, `amount`, `context_json` | `{ account, patch }`. | +| `bank:hot:withdraw` | `uid`, `amount`, `context_json` | `{ account, patch }`. | +| `bank:hot:deposit_earnings` | `uid`, `amount`, `context_json` | `{ account, patch }`. | +| `bank:hot:transfer` | `source_uid`, `target_uid`, `amount`, `context_json` | Transfer result JSON. | +| `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. | +| `bank:hot:validate_pin` | `uid`, `pin`, `context_json` | `{}` on success. | +| `bank:hot:change_pin` | `uid`, `current_pin`, `new_pin`, `context_json` | `{ account, patch }`. | +| `bank:hot:save` | `uid` | Current hot bank JSON and async durable save. | +| `bank:hot:remove` | `uid` | `OK`. | + +Use hot-state commands for UI workflows. They return patch objects so the UI can +update only changed fields. + +## Deposit and Withdraw + +ATM sessions require `atmAuthorized: true`. Full bank sessions can set +`mode: "bank"`. + +```sqf +private _context = createHashMapFromArray [ + ["mode", "atm"], + ["atmAuthorized", true] +]; + +private _deposit = "forge_server" callExtension ["bank:hot:deposit", [ + getPlayerUID player, + "100", + toJSON _context +]]; + +private _withdraw = "forge_server" callExtension ["bank:hot:withdraw", [ + getPlayerUID player, + "50", + toJSON _context +]]; +``` + +## Transfer + +Transfers are only available from the full bank interface. `fromField` can be +`bank` or `cash`. + +```sqf +private _context = createHashMapFromArray [ + ["mode", "bank"], + ["atmAuthorized", false], + ["fromField", "bank"] +]; + +private _result = "forge_server" callExtension ["bank:hot:transfer", [ + getPlayerUID player, + _targetUid, + "250", + toJSON _context +]]; +``` + +## Checkout Charge + +Checkout charging supports `sourceField: "cash"` or `sourceField: "bank"`. +Set `commit` to `false` to preview the patch without saving. + +```sqf +private _context = createHashMapFromArray [ + ["sourceField", "bank"], + ["commit", true] +]; + +private _result = "forge_server" callExtension ["bank:hot:charge_checkout", [ + getPlayerUID player, + "125", + toJSON _context +]]; +``` + +## PIN Validation + +PIN entry is only valid in ATM mode. + +```sqf +private _context = createHashMapFromArray [["mode", "atm"]]; + +private _result = "forge_server" callExtension ["bank:hot:validate_pin", [ + getPlayerUID player, + "1234", + toJSON _context +]]; +``` + +## PIN Changes + +PIN changes require the current PIN and a different four-digit new PIN. The +command is only valid from the full bank interface. + +```sqf +private _context = createHashMapFromArray [ + ["mode", "bank"], + ["atmAuthorized", false] +]; + +private _result = "forge_server" callExtension ["bank:hot:change_pin", [ + getPlayerUID player, + "1234", + "5678", + toJSON _context +]]; +``` + +## Error Handling + +```sqf +private _result = "forge_server" callExtension ["bank:hot:get", [getPlayerUID player]]; +private _payload = _result select 0; + +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Bank error: %1", _payload]; +}; + +private _bank = fromJSON _payload; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CAD_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CAD_USAGE_GUIDE.md new file mode 100644 index 0000000..e8e3663 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CAD_USAGE_GUIDE.md @@ -0,0 +1,191 @@ +# CAD Usage Guide + +The CAD module stores transient operational state for dispatch activity, +assignments, dispatch orders, support requests, group profiles, grouped views, +and hydrated UI payloads. CAD state is in-memory and follows the active server +or mission lifecycle. + +## Data Model + +Most CAD records are flexible JSON objects. The service normalizes important +IDs and returns structured mutation results for higher-level workflows. + +Common generated IDs: + +- Orders: `cad-order:` +- Requests: `cad-request:` +- Assignments usually share a task ID or order ID. + +## Commands + +### Activity + +| Command | Arguments | Returns | +| --- | --- | --- | +| `cad:activity:append` | `activity_json` | `OK`. | +| `cad:activity:recent` | `limit` | Recent activity array JSON. | + +### Assignments + +| Command | Arguments | Returns | +| --- | --- | --- | +| `cad:assignments:list` | none | Assignment array JSON. | +| `cad:assignments:assign` | `entry_id`, `assignment_json` | Assignment mutation result JSON. | +| `cad:assignments:acknowledge` | `entry_id`, `patch_json` | Assignment mutation result JSON. | +| `cad:assignments:decline` | `entry_id`, `patch_json` | Assignment mutation result JSON and removes assignment. | +| `cad:assignments:upsert` | `entry_id`, `assignment_json` | `OK`. | +| `cad:assignments:delete` | `entry_id` | `OK`. | + +### Orders + +| Command | Arguments | Returns | +| --- | --- | --- | +| `cad:orders:list` | none | Order array JSON. | +| `cad:orders:create` | `order_seed_json` | Dispatch order mutation result JSON. | +| `cad:orders:create_from_context` | `context_json` | Dispatch order mutation result JSON. | +| `cad:orders:close` | `entry_id` | Dispatch order mutation result JSON and removes order/assignment. | +| `cad:orders:upsert` | `entry_id`, `order_json` | `OK`. | +| `cad:orders:delete` | `entry_id` | `OK`. | + +### Requests + +| Command | Arguments | Returns | +| --- | --- | --- | +| `cad:requests:list` | none | Request array JSON. | +| `cad:requests:submit` | `request_json` | Request mutation result JSON. | +| `cad:requests:submit_from_context` | `context_json` | Request mutation result JSON. | +| `cad:requests:close` | `entry_id` | Request mutation result JSON and removes request. | +| `cad:requests:upsert` | `entry_id`, `request_json` | `OK`. | +| `cad:requests:delete` | `entry_id` | `OK`. | + +### Profiles and Views + +| Command | Arguments | Returns | +| --- | --- | --- | +| `cad:profiles:list` | none | Profile array JSON. | +| `cad:profiles:update_from_context` | `context_json` | Profile mutation result JSON. | +| `cad:profiles:upsert` | `entry_id`, `profile_json` | `OK`. | +| `cad:profiles:delete` | `entry_id` | `OK`. | +| `cad:groups:build` | `groups_seed_json` | Group array JSON. | +| `cad:view:hydrate` | `hydrate_seed_json` | Hydrated CAD payload JSON. | + +## Submit a Support Request + +```sqf +private _fields = createHashMapFromArray [ + ["pickup_location", "Grid 123456"], + ["precedence", "urgent"], + ["security", "secure"] +]; + +private _context = createHashMapFromArray [ + ["type", "medevac_9line"], + ["fields", _fields], + ["groupId", "alpha"], + ["groupCallsign", "Alpha 1-1"], + ["submittedByUid", getPlayerUID player], + ["submittedByName", name player], + ["priority", "emergency"], + ["position", getPosATL player], + ["createdAt", diag_tickTime] +]; + +private _result = "forge_server" callExtension ["cad:requests:submit_from_context", [ + toJSON _context +]]; +``` + +Supported priority values are `routine`, `priority`, and `emergency`. Unknown +values normalize to `priority`. + +## Create a Dispatch Order + +```sqf +private _context = createHashMapFromArray [ + ["assigneeGroupId", "bravo"], + ["assigneeGroupCallsign", "Bravo 1-1"], + ["targetGroupId", "alpha"], + ["targetGroupCallsign", "Alpha 1-1"], + ["targetPosition", getPosATL player], + ["createdByUid", getPlayerUID player], + ["createdByName", name player], + ["requestId", "cad-request:1"], + ["requestType", "logreq"], + ["requestTitle", "LOGREQ | Alpha 1-1"], + ["requestSummary", "Ammo resupply requested"], + ["requestFields", createHashMap], + ["note", "Support Alpha 1-1 at current position."], + ["priority", "priority"], + ["createdAt", diag_tickTime] +]; + +private _result = "forge_server" callExtension ["cad:orders:create_from_context", [ + toJSON _context +]]; +``` + +## Assignment Workflow + +Task contracts have two separate phases. Dispatch assignment reserves a +contract for a group and sets the CAD assignment state to `assigned`, but it +does not accept or start the task. The assigned group leader must acknowledge +the assignment before task ownership is bound and task logic starts. If the +leader declines, the CAD assignment is removed and the contract returns to the +open board. Task status follows the same lifecycle: `available` on creation, +`assigned` after dispatch assignment, and `active` after acknowledgement. + +```sqf +private _assignment = createHashMapFromArray [ + ["groupId", "bravo"], + ["assigneeGroupCallsign", "Bravo 1-1"], + ["assignedByUid", getPlayerUID player], + ["assignedByName", name player], + ["assignedAt", diag_tickTime], + ["state", "assigned"] +]; + +"forge_server" callExtension ["cad:assignments:assign", [ + "task-123", + toJSON _assignment +]]; + +private _ack = createHashMapFromArray [ + ["state", "acknowledged"], + ["acknowledgedByUid", getPlayerUID player], + ["acknowledgedAt", diag_tickTime] +]; + +"forge_server" callExtension ["cad:assignments:acknowledge", [ + "task-123", + toJSON _ack +]]; +``` + +## Hydrate the CAD UI + +```sqf +private _session = createHashMapFromArray [ + ["uid", getPlayerUID player], + ["orgId", "default"], + ["isDispatcher", true], + ["groupId", "alpha"], + ["isLeader", true] +]; + +private _seed = createHashMapFromArray [ + ["groups", _liveGroups], + ["activeTasks", _activeTasks], + ["session", _session] +]; + +private _result = "forge_server" callExtension ["cad:view:hydrate", [toJSON _seed]]; +``` + +## Error Handling + +```sqf +private _payload = _result select 0; +if (_payload find "Error:" == 0) exitWith { + systemChat format ["CAD error: %1", _payload]; +}; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_ACTOR_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_ACTOR_USAGE_GUIDE.md new file mode 100644 index 0000000..dfda823 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_ACTOR_USAGE_GUIDE.md @@ -0,0 +1,98 @@ +# Client Actor Usage Guide + +The client actor addon owns the player interaction menu and client-side actor +repository. It is the main launcher for nearby player actions and other Forge +client UIs. + +## Open the Actor Menu + +```sqf +call forge_client_actor_fnc_openUI; +``` + +The actor menu opens `RscActorMenu`, loads `ui/_site/index.html`, and routes +browser alerts through `forge_client_actor_fnc_handleUIEvents`. + +## Repository + +`forge_client_actor_fnc_initRepository` creates `GVAR(ActorRepository)`. + +The repository: + +- requests actor initialization from the server +- saves actor state through the server actor addon +- caches client-visible actor fields +- applies position, direction, stance, rank, and loadout on JIP sync when the + relevant settings allow it +- provides nearby interaction actions to the browser UI + +Initialize actor state through the repository: + +```sqf +GVAR(ActorRepository) call ["init", []]; +``` + +Save actor state through the server: + +```sqf +GVAR(ActorRepository) call ["save", [true]]; +``` + +## Nearby Actions + +The menu asks for nearby actions with: + +```text +actor::get::actions +``` + +The repository scans objects within 5 meters and returns actions based on +mission object variables: + +| Variable | Action | +| --- | --- | +| `isStore` | store | +| `isAtm` | ATM | +| `isBank` | bank | +| `isGarage` | garage | +| `garageType` | garage subtype | +| `isLocker` | virtual arsenal action when VA is enabled | +| `deviceType` | device action placeholder | +| nearby player unit | player interaction placeholder | + +The response is pushed into the browser with `updateAvailableActions(...)`. + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `actor::get::actions` | Refresh nearby actions. | +| `actor::close::menu` | Close actor menu. | +| `actor::open::atm` | Open bank UI in ATM mode. | +| `actor::open::bank` | Open bank UI in bank mode. | +| `actor::open::cad` | Open CAD UI. | +| `actor::open::garage` | Open garage UI. | +| `actor::open::vgarage` | Open virtual garage. | +| `actor::open::org` | Open organization UI. | +| `actor::open::vlocker` | Open ACE arsenal on `FORGE_Locker_Box`. | +| `actor::open::phone` | Open phone UI. | +| `actor::open::store` | Open store UI. | + +Device and player interaction events currently display placeholder feedback. + +## Authoritative State + +Actor persistence is server-owned. The client repository requests and displays +actor data, but actor creation, durable updates, and hot-state behavior are +handled by the server actor addon and extension. + +## Related Guides + +- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_BANK_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_BANK_USAGE_GUIDE.md new file mode 100644 index 0000000..8339b18 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_BANK_USAGE_GUIDE.md @@ -0,0 +1,89 @@ +# Client Bank Usage Guide + +The client bank addon opens the bank and ATM browser UI, forwards banking +requests to the server bank addon, and pushes account updates back into the +browser. + +## Open Bank UI + +Open full bank mode: + +```sqf +call forge_client_bank_fnc_openUI; +``` + +Open ATM mode: + +```sqf +[true] call forge_client_bank_fnc_openUI; +``` + +The open function creates `RscBank`, sets the bridge mode to `bank` or `atm`, +loads `ui/_site/index.html`, and routes browser events through +`forge_client_bank_fnc_handleUIEvents`. + +## Bridge and Repository + +`forge_client_bank_fnc_initRepository` tracks account load and cached account +state. + +`forge_client_bank_fnc_initUIBridge` owns: + +- active browser control tracking +- bank/ATM mode +- browser ready handling +- account hydrate and sync responses +- deposit, withdrawal, transfer, earnings deposit, credit repayment, PIN + validation, and PIN change requests +- browser notice delivery + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `bank::ready` | Mark browser ready and request hydrate from the server. | +| `bank::refresh` | Request fresh bank hydrate data. | +| `bank::deposit::request` | Forward deposit amount to the server. | +| `bank::withdraw::request` | Forward withdrawal amount to the server. | +| `bank::transfer::request` | Forward target, source field, and amount. | +| `bank::depositEarnings::request` | Request earnings deposit. | +| `bank::repayCreditLine::request` | Request credit-line repayment. | +| `bank::pin::request` | Forward PIN validation request. | +| `bank::pin::change::request` | Forward current and new PIN values for a PIN change. | +| `bank::close` | Dispose bridge screen state and close the display. | + +## Browser Response Events + +The bridge sends: + +| Event | Purpose | +| --- | --- | +| `bank::hydrate` | Full session/account payload. | +| `bank::sync` | Account patch or sync data. | +| `bank::notice` | UI-visible notice payload. | + +## Request Flow + +Example deposit flow: + +1. Browser sends `bank::deposit::request` with an `amount`. +2. Client bridge calls the server bank request event. +3. Server bank addon validates the request and calls bank hot-state logic. +4. Server response is caught by the client post-init event handlers. +5. Client bridge sends `bank::sync` or `bank::notice` back to the browser. + +## Authoritative State + +Balances, PIN authorization, transfers, checkout charges, credit lines, and +persistence are server-owned. The client should only display account data and +request mutations through server events. + +PIN changes are available from the full bank UI only. The browser validates the +current, new, and confirmation fields, but the server extension remains +authoritative and persists the updated PIN. + +## Related Guides + +- [Bank Usage Guide](./BANK_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_CAD_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_CAD_USAGE_GUIDE.md new file mode 100644 index 0000000..ecd19f2 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_CAD_USAGE_GUIDE.md @@ -0,0 +1,111 @@ +# Client CAD Usage Guide + +The client CAD addon provides the map and dispatch UI for groups, active +tasks, task assignment, dispatch orders, support requests, and task +acknowledge/decline workflows. + +## Open CAD UI + +```sqf +call forge_client_cad_fnc_openUI; +``` + +The CAD UI opens `RscMapUI` and loads separate browser controls for: + +- top bar +- bottom bar +- side panel +- dispatcher board + +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. + +`forge_client_cad_fnc_initUIBridge` owns: + +- ready state for side panel, top bar, and dispatcher board +- operations vs dispatch mode +- board vs map dispatch view +- hydrate requests +- task assignment, acknowledge, and decline requests +- dispatch order create/close requests +- support request submit/close requests +- group status, role, and profile requests +- map focus actions + +## Map Focus Behavior + +CAD list entries can drive the native map position without duplicating map +logic in the browser UI. In operations mode, assigned or accepted task cards, +roster member cards, and support request cards send focus events. In dispatch +map mode, group, contract, and support request cards use the same focus path. + +Task and support request focus uses the stored record position. Roster member +focus uses the member position included in the hydrated group roster. + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `cad::topbar::ready` | Mark top bar ready and push top bar state. | +| `cad::ready` | Mark side panel ready and request hydrate. | +| `cad::dispatcher::ready` | Mark dispatcher board ready and push hydrate data. | +| `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::tasks::assign` | Assign a task to a group. | +| `cad::tasks::acknowledge` | Acknowledge assigned task. | +| `cad::tasks::decline` | Decline assigned task. | +| `cad::dispatchOrder::create` | Create dispatch order. | +| `cad::dispatchOrder::close` | Close dispatch order. | +| `cad::supportRequest::submit` | Submit support request. | +| `cad::supportRequest::close` | Close support request. | +| `cad::groups::status` | Update group status. | +| `cad::groups::role` | Update group role. | +| `cad::groups::profile` | Update status and role together. | +| `cad::groups::focus` | Center map on a group. | +| `cad::members::focus` | Center map on a group member. | +| `cad::tasks::focus` | Center map on a task. | +| `cad::requests::focus` | Center map on a support request. | +| `map::zoomIn` | Zoom native map in. | +| `map::zoomOut` | Zoom native map out. | +| `map::search` | Placeholder status update. | +| `map::close` | Dispose bridge state and close the display. | + +## Response Events + +The bridge pushes: + +| Event | Purpose | +| --- | --- | +| `cad::hydrate` | Full hydrated CAD payload to the side panel. | +| `cad::assignment::response` | Task assignment/acknowledge/decline result. | +| `cad::group::response` | Group status/role/profile result. | +| `cad::request::response` | Support request result. | + +Dispatcher board controls also receive direct `ExecJS` status and hydrate +calls. + +## Task Compatibility + +CAD task visibility depends on server-side task catalog entries. Tasks created +through Eden Forge task modules or `forge_task_fnc_startTask` are the +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. + +## Authorization Notes + +Only dispatcher sessions can enter dispatch mode. If the hydrated session is +not a dispatcher, the bridge forces the UI back to operations mode. + +## Related Guides + +- [CAD Usage Guide](./CAD_USAGE_GUIDE.md) +- [Task Usage Guide](./TASK_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_COMMON_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_COMMON_USAGE_GUIDE.md new file mode 100644 index 0000000..754d6c8 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_COMMON_USAGE_GUIDE.md @@ -0,0 +1,92 @@ +# Client Common Usage Guide + +The client `common` addon contains shared browser UI bridge declarations and +common client-side browser integration patterns. + +## Purpose + +Use `forge_client_common` when a browser-backed feature UI needs reusable +screen lifecycle behavior: + +- active browser control tracking +- browser ready state +- pending event queues +- `ExecJS` payload delivery +- shared bridge object inheritance through `createHashMapObject` + +Feature addons still own their app-specific events and server RPC mapping. + +## Shared Bridge + +Initialize the bridge declarations with: + +```sqf +private _webUIDeclarations = call forge_client_common_fnc_initWebUIBridge; +private _bridgeDeclaration = _webUIDeclarations get "bridgeDeclaration"; +``` + +Feature bridges can inherit from the shared declaration: + +```sqf +GVAR(MyUIBridgeBaseClass) = compileFinal createHashMapFromArray [ + ["#base", _bridgeDeclaration], + ["#type", "MyUIBridgeBaseClass"], + ["handleReady", compileFinal { + params [["_control", controlNull, [controlNull]]]; + + _self call ["setActiveBrowserControl", [_control]]; + _self call ["sendEvent", ["myAddon::hydrate", createHashMap, _control]]; + }] +]; +``` + +## Event Delivery + +`sendEvent` builds this payload: + +```json +{ + "event": "myAddon::event", + "data": {} +} +``` + +If the browser control is missing or not ready, the payload is queued on the +screen object. When the screen marks ready, `flushPendingEvents` delivers the +queue. + +## Screen Lifecycle + +The shared screen object tracks: + +| Field | Purpose | +| --- | --- | +| `control` | Active browser control. | +| `readyState` | Whether the browser app has sent its ready event. | +| `pendingEvents` | Outbound events waiting for a ready browser. | + +Call `handleClose` or `dispose` when a display closes so stale controls and +queued events are cleared. + +## Current Consumers + +The common bridge pattern is used by the newer bank, CAD, garage, and +organization client bridges. Store currently keeps its own bridge object and +browser bridge function names. + +## Usage Rules + +- Keep bridge inheritance in feature addons thin and explicit. +- Keep shared code generic; do not add bank, CAD, org, or store-specific logic + to `common`. +- Prefer namespaced events such as `garage::sync`. +- Send hash maps or arrays that can be safely serialized with `toJSON`. +- Avoid direct extension calls from the client bridge; send CBA server events. + +## Related Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_GARAGE_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_GARAGE_USAGE_GUIDE.md new file mode 100644 index 0000000..f076ac1 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_GARAGE_USAGE_GUIDE.md @@ -0,0 +1,114 @@ +# Client Garage Usage Guide + +The client garage addon provides player vehicle storage UI, vehicle +store/retrieve actions, selected nearby vehicle service requests, vehicle +context building, and the virtual garage view. + +## Open Garage UI + +```sqf +call forge_client_garage_fnc_openUI; +``` + +The garage UI opens `RscGarage`, loads `ui/_site/index.html`, and routes +browser events through `forge_client_garage_fnc_handleUIEvents`. + +## Open Virtual Garage + +```sqf +call forge_client_garage_fnc_openVG; +``` + +The virtual garage resolves the active interaction object near the player, +discovers nearby `garage*` markers placed in Eden, chooses the matching spawn +lane for the selected vehicle type, opens the BIS garage interface, and +restricts the available vehicle lists from the virtual garage repository. When +the BIS garage closes, only the vehicle selected in that virtual garage session +is finalized and spawned onto the resolved lane. + +## Client Services + +| Service | Purpose | +| --- | --- | +| `GarageRepository` | Player garage view state. | +| `VGRepository` | Virtual garage unlock view state. | +| `GarageHelperService` | Vehicle names, hit points, and payload helpers. | +| `GarageContextService` | Nearby/current vehicle context. | +| `GaragePayloadService` | Browser hydrate payload construction. | +| `GarageActionService` | Store/retrieve request handling and selected nearby vehicle refuel/repair request forwarding. | +| `GarageUIBridge` | Browser ready, hydrate, and sync delivery. | + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `garage::ready` | Mark browser ready and send `garage::hydrate`. | +| `garage::refresh` | Send current garage payload as `garage::sync`. | +| `garage::vehicle::retrieve::request` | Forward retrieve request through the action service. | +| `garage::vehicle::store::request` | Forward store request through the action service. | +| `garage::vehicle::refuel::request` | Forward selected nearby vehicle refuel request to the server economy service. | +| `garage::vehicle::repair::request` | Forward selected nearby vehicle repair request to the server economy service. | +| `garage::close` | Dispose bridge screen state and close the display. | + +## Browser Response Events + +| Event | Purpose | +| --- | --- | +| `garage::hydrate` | Initial vehicle and session payload. | +| `garage::sync` | Refreshed vehicle payload. | +| `garage::service::success` | Browser notice for accepted refuel/rearm/repair requests. | +| `garage::service::failure` | Browser notice for rejected refuel/rearm/repair requests. | + +Server action responses are handled by the action service and notification +flow. + +## Vehicle Service + +The selected vehicle detail panel includes refuel, rearm, and repair actions for nearby +world vehicles. Stored records must be retrieved first because server economy +services operate on live vehicle objects, not stored garage records. + +Refuel requests use the server economy `RefuelService` event. Rearm requests +use `RearmService`. Repair requests use `RepairService`. These services are +billed by the server economy addon through organization funds. + +## Mission Setup + +Garage interactions are normally surfaced through the actor menu when nearby +objects have garage variables such as: + +```sqf +_object setVariable ["isGarage", true, true]; +_object setVariable ["garageType", "cars", true]; +``` + +When using the server garage auto-init flow, editor-placed objects whose +variable names contain `garage` are marked as garage interaction points and +their `garageType` can be inferred from the name. + +Virtual garage spawn lanes are resolved from empty markers placed in Eden. The +marker name should contain `garage` and one of the six supported category names: +`cars`, `armor`, `helis`, `planes`, `naval`, or `other`. Markers are matched to +the nearby interaction object by proximity, and names that include the garage +object's variable name are preferred when multiple garages exist. + +Vehicle spawning is strict by category. If the active garage site does not have +a matching local marker for the vehicle category being retrieved or spawned from +the virtual garage, the request is blocked and the player is shown a message. + +Nearby world vehicles are not used as virtual garage spawn candidates. They are +only checked to determine whether the resolved spawn position is blocked. If +any vehicle is within 5 meters of the spawn marker when the virtual garage is +opened, the session is blocked and the player is shown a warning. + +## Authoritative State + +The client gathers vehicle context and sends store/retrieve requests. Stored +vehicle state, validation, spawning, removal, and persistence are owned by the +server garage addon and extension. + +## Related Guides + +- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_LOCKER_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_LOCKER_USAGE_GUIDE.md new file mode 100644 index 0000000..dc8d724 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_LOCKER_USAGE_GUIDE.md @@ -0,0 +1,87 @@ +# Client Locker Usage Guide + +The client locker addon manages personal locker display state, local locker +container behavior, and virtual arsenal unlock state. + +## Repositories + +`forge_client_locker_fnc_initRepository` creates `GVAR(LockerRepository)`. + +`forge_client_locker_fnc_initVARepository` creates `GVAR(VARepository)`. + +Initialize locker state: + +```sqf +GVAR(LockerRepository) call ["init", []]; +GVAR(VARepository) call ["init", []]; +``` + +## Locker Container Flow + +The repository searches mission namespace variables whose names contain +`locker` and refer to objects. For each server/mission locker object, it creates +a local `Box_NATO_Equip_F` at the same position and attaches container event +handlers. + +On container open: + +- the local container is cleared +- cached locker items are inserted into the container +- over-capacity warnings are emitted when the item count is above 25 + +On container close: + +- cargo, nested container items, and weapon attachments are read back +- the new locker map is sent to the server with the override request +- the local repository cache is updated + +## Virtual Arsenal Flow + +The virtual arsenal repository creates a local `FORGE_Locker_Box` and requests +virtual arsenal unlocks from the server. + +As sync data arrives, it applies unlocks through ACE Arsenal: + +| Data key | Client behavior | +| --- | --- | +| `items` | Add virtual items. | +| `weapons` | Add virtual weapons. | +| `magazines` | Add virtual magazines. | +| `backpacks` | Add virtual backpacks. | + +The actor menu opens the virtual locker with: + +```sqf +[FORGE_Locker_Box, player, false] spawn ace_arsenal_fnc_openBox; +``` + +## Server Events + +The client repository sends requests for: + +- locker initialization +- locker save +- locker override after container close +- virtual arsenal initialization +- virtual arsenal save + +The server locker addon and extension own the saved locker and virtual arsenal +state. + +## Mission Setup + +Mission locker objects must be placed into `missionNamespace` with a variable +name containing `locker`. The client creates local interactive containers from +those authoritative mission objects. + +Example: + +```sqf +missionNamespace setVariable ["forge_locker_alpha", _lockerObject, true]; +``` + +## Related Guides + +- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) +- [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_MAIN_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_MAIN_USAGE_GUIDE.md new file mode 100644 index 0000000..0ac8fbf --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_MAIN_USAGE_GUIDE.md @@ -0,0 +1,48 @@ +# Client Main Usage Guide + +The client `main` addon provides the shared mod identity, version metadata, +CBA settings, and macro foundation used by the Forge client addons. + +## Purpose + +Use `forge_client_main` as the foundation dependency for client addons that +need Forge macros, function naming, settings, or mod-level configuration. + +Feature logic should stay in the owning addon. `main` should remain limited to +shared client configuration and compile infrastructure. + +## Key Files + +| File | Purpose | +| --- | --- | +| `script_mod.hpp` | Client mod identity. | +| `script_version.hpp` | Client mod version values. | +| `script_macros.hpp` | Shared client macros. | +| `CfgSettings.hpp` | Client CBA settings. | +| `config.cpp` | Addon config and mod wiring. | + +## Dependency Pattern + +Feature addons normally depend on `forge_client_main` in their `config.cpp`. + +```cpp +class forge_client_example { + requiredAddons[] = { + "forge_client_main" + }; +}; +``` + +## Usage Notes + +- Put domain UI, repositories, and event handling in feature addons. +- Put reusable browser bridge behavior in `forge_client_common`. +- Put server-only behavior in `arma/server/addons`. +- Keep settings in `CfgSettings.hpp` when they apply to the client mod as a + whole or to a client feature toggle. + +## Related Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Development Guide](./DEVELOPMENT_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md new file mode 100644 index 0000000..e102583 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md @@ -0,0 +1,74 @@ +# Client Notifications Usage Guide + +The client notifications addon owns the notification HUD, notification sound, +and local notification service used by Forge client and server modules. + +## Runtime Behavior + +The notification display is created during client initialization. The browser +HUD sends: + +```text +notifications::ready +``` + +When that event is received, `NotificationService` initializes and sends a +startup notification. + +## Create a Notification + +Use the notification service when available: + +```sqf +GVAR(NotificationService) call ["create", [ + "success", + "Title", + "Notification text.", + 4000 +]]; +``` + +Arguments: + +| Argument | Purpose | +| --- | --- | +| `_type` | Notification type, such as `success`, `info`, `warning`, or `error`. | +| `_title` | Notification title. | +| `_content` | Notification body text. | +| `_duration` | Display duration in milliseconds. | + +The service dispatches a browser `forge:notify` custom event. + +## CBA Event Surface + +Other addons can use the client notification event: + +```sqf +["forge_client_notifications_recieveNotification", [ + "warning", + "Garage", + "Vehicle spawn position is blocked.", + 3000 +]] call CBA_fnc_localEvent; +``` + +The event payload is: + +```sqf +[_type, _title, _content, _duration] +``` + +## Usage Rules + +- Use the shared notification service instead of opening separate transient + browser UIs. +- Keep server-driven player feedback short and actionable. +- Treat notification state as transient client UI state. +- Do not use notifications as the only record of durable domain changes. + +## Related Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_ORG_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_ORG_USAGE_GUIDE.md new file mode 100644 index 0000000..2340a03 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_ORG_USAGE_GUIDE.md @@ -0,0 +1,131 @@ +# Client Organization Usage Guide + +The client organization addon provides the organization portal UI and browser +bridge for login, registration, membership, invites, credit lines, leave and +disband flows, assets, fleet, and treasury display. + +Organization registration requires $50,000 in personal funds. The server org +addon enforces and charges the fee; the browser only displays the requirement +and submits the registration request. + +## Open Organization UI + +```sqf +call forge_client_org_fnc_openUI; +``` + +The UI opens `RscOrg`, loads `ui/_site/index.html`, and routes browser alerts +through `forge_client_org_fnc_handleUIEvents`. + +## Repository and Bridge + +`forge_client_org_fnc_initRepository` caches organization portal state. + +`forge_client_org_fnc_initUIBridge` owns: + +- active browser control tracking +- portal hydrate requests +- create/login response routing +- leave and disband requests +- credit-line assignment requests +- payroll and treasury transfer requests +- invite, accept invite, and decline invite requests +- targeted browser response events + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `org::ready` | Mark browser ready and request `org::sync`. | +| `org::login::request` | Request portal hydrate as `org::login::success`. | +| `org::create::request` | Validate org name and request creation on server. | +| `org::disband::request` | Request disband on server. | +| `org::leave::request` | Request leave on server. | +| `org::credit::request` | Request credit-line assignment. | +| `org::payroll::request` | Request payroll payout from the organization treasury. | +| `org::transfer::request` | Request treasury transfer to a member. | +| `org::invite::request` | Request member invite. | +| `org::invite::accept` | Accept invite by org ID. | +| `org::invite::decline` | Decline invite by org ID. | +| `org::close` | Close the display. | + +## Browser Response Events + +| Event | Purpose | +| --- | --- | +| `org::sync` | Full portal sync payload. | +| `org::login::success` | Login hydrate payload. | +| `org::create::success` | Creation hydrate payload. | +| `org::create::failure` | Creation validation or server failure. | +| `org::disband::success` | Requester disband success. | +| `org::disband::failure` | Disband failure. | +| `org::portal::revoked` | Portal state revoked by someone else's disband action. | +| `org::leave::success` | Leave success. | +| `org::leave::failure` | Leave failure. | +| `org::credit::success` | Credit-line request success. | +| `org::credit::failure` | Credit-line request failure. | +| `org::member::creditUpdated` | Targeted member credit-line patch. | +| `org::invite::success` | Invite success. | +| `org::invite::failure` | Invite failure. | +| `org::invite::decision::success` | Invite accept/decline success. | +| `org::invite::decision::failure` | Invite accept/decline failure. | + +## Request Examples + +Create organization request payload: + +```json +{ + "orgName": "Example Logistics" +} +``` + +Credit-line request payload: + +```json +{ + "memberUid": "76561198000000000", + "memberName": "Player Name", + "amount": 2500 +} +``` + +Payroll request payload: + +```json +{ + "amount": 1000 +} +``` + +Treasury transfer request payload: + +```json +{ + "memberUid": "76561198000000000", + "memberName": "Player Name", + "amount": 1000 +} +``` + +Invite request payload: + +```json +{ + "targetUid": "76561198000000000", + "targetName": "Player Name" +} +``` + +## Authoritative State + +Organization funds, reputation, membership, invites, credit lines, assets, +fleet, and persistence are server-owned. The client portal only displays and +requests changes. + +## Related Guides + +- [Organization Usage Guide](./ORG_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_PHONE_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_PHONE_USAGE_GUIDE.md new file mode 100644 index 0000000..f085d7d --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_PHONE_USAGE_GUIDE.md @@ -0,0 +1,108 @@ +# Client Phone Usage Guide + +The client phone addon provides the in-game phone UI for contacts, SMS +messages, email, and local utility apps such as notes, calendar events, world +clocks, and alarms. + +## Open Phone UI + +```sqf +call forge_client_phone_fnc_openUI; +``` + +The phone UI creates `RscPhone`, loads `ui/_site/index.html`, and routes +browser alerts through `forge_client_phone_fnc_handleUIEvents`. + +## State Ownership + +Contacts, messages, and emails are server-owned and requested through the +server phone addon. + +Local utility app state is stored in `profileNamespace`: + +- notes +- calendar events +- world clocks +- alarms +- theme/preferences + +## Phone Repository + +`forge_client_phone_fnc_initRepository` creates `GVAR(PhoneRepository)`. + +The phone repository owns local notes, events, clocks, alarms, and settings +helpers. +Contacts, messages, and emails continue to use server-backed request/response +events. + +## Browser Events + +### Session and Preferences + +| Event | Client behavior | +| --- | --- | +| `phone::get::player` | Send player UID to browser with `setPlayerUid`. | +| `phone::get::theme` | Send saved light/dark theme to browser. | +| `phone::set::theme` | Save theme preference to `profileNamespace`. | + +### Contacts + +| Event | Client behavior | +| --- | --- | +| `phone::get::contacts` | Load cached contacts and request server refresh. | +| `phone::refresh::contacts` | Request contacts from server. | +| `phone::add::contact` | Add contact by phone number. | +| `phone::add::contact::by::phone` | Add contact by phone number. | +| `phone::add::contact::by::email` | Add contact by email. | +| `phone::remove::contact` | Remove contact by UID. | + +### Messages + +| Event | Client behavior | +| --- | --- | +| `phone::get::messages` | Request messages from server. | +| `phone::get::message::thread` | Request thread with another UID. | +| `phone::send::message` | Send SMS through server. | +| `phone::mark::message::read` | Mark message read on server. | +| `phone::delete::message` | Delete message on server. | + +### Email + +| Event | Client behavior | +| --- | --- | +| `phone::get::emails` | Request emails from server. | +| `phone::send::email` | Send email through server. | +| `phone::mark::email::read` | Mark email read on server. | +| `phone::delete::email` | Delete email on server. | + +### Local Utility Apps + +| Event | Client behavior | +| --- | --- | +| `phone::get::notes` | Load local notes. | +| `phone::save::note` | Save local note. | +| `phone::delete::note` | Delete local note. | +| `phone::get::events` | Load local calendar events. | +| `phone::save::event` | Save local calendar event. | +| `phone::delete::event` | Delete local calendar event. | +| `phone::get::clocks` | Load local world clocks. | +| `phone::save::clock` | Save local world clock. | +| `phone::delete::clock` | Delete local world clock. | +| `phone::get::alarms` | Load local alarms. | +| `phone::save::alarm` | Save local alarm. | +| `phone::delete::alarm` | Delete local alarm. | +| `phone::toggle::alarm` | Toggle local alarm enabled state. | + +## Usage Rules + +- Send contact, message, and email mutations to the server phone addon. +- Keep local-only utility apps in `profileNamespace` until they are migrated to + server-backed storage. +- Do not treat local phone utility state as shared multiplayer state. +- Validate required UID, phone, email, subject, and message fields before + sending server requests. + +## Related Guides + +- [Phone Usage Guide](./PHONE_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_STORE_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_STORE_USAGE_GUIDE.md new file mode 100644 index 0000000..3078d3c --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_STORE_USAGE_GUIDE.md @@ -0,0 +1,92 @@ +# Client Store Usage Guide + +The client store addon provides the storefront browser UI for catalog browsing, +category hydration, payment source display, cart handling, and checkout +requests. + +## Open Store UI + +```sqf +call forge_client_store_fnc_openUI; +``` + +The UI opens `RscStore`, loads `ui/_site/index.html`, and routes browser alerts +through `forge_client_store_fnc_handleUIEvents`. + +## Bridge + +`forge_client_store_fnc_initUIBridge` owns: + +- browser control lookup +- store hydrate requests +- category requests +- checkout requests +- category hydrate/failure responses +- checkout success/failure responses +- store config refresh after successful checkout + +Store currently uses its own `StoreUIBridge.receive(...)` browser bridge rather +than the shared `ForgeBridge.receive(...)` delivery used by newer bridges. + +## Browser Events + +| Event | Client behavior | +| --- | --- | +| `store::ready` | Request store hydrate from the server. | +| `store::category::request` | Request catalog items for a category. | +| `store::checkout::request` | Forward checkout JSON to the server. | +| `store::close` | Close the display. | + +## Browser Response Events + +| Event | Purpose | +| --- | --- | +| `store::hydrate` | Initial storefront/session/config payload. | +| `store::config::hydrate` | Refreshed payment/source config. | +| `store::category::hydrate` | Category catalog payload. | +| `store::category::failure` | Category request failure. | +| `store::checkout::success` | Checkout success payload. | +| `store::checkout::failure` | Checkout failure payload. | + +## Category Requests + +Category requests require a non-empty category value. + +```json +{ + "category": "weapons" +} +``` + +The client lowercases the category before forwarding it to the server store +addon. + +## Checkout Requests + +Checkout requests send a serialized checkout payload: + +```json +{ + "checkoutJson": "{\"items\":[],\"paymentSource\":\"cash\"}" +} +``` + +The client only forwards the checkout data. The server store addon and +extension validate prices, inventory grants, payment source authorization, and +integration with bank, organization, locker, and garage state. + +After a successful checkout, the client asks the server for a fresh store config +payload so payment-source balances and permissions stay current. + +## Authoritative State + +Catalog data, prices, checkout validation, money movement, organization funds, +credit lines, locker grants, garage grants, and persistence are server-owned. + +## Related Guides + +- [Store Usage Guide](./STORE_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_USAGE_GUIDE.md new file mode 100644 index 0000000..23f74f1 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/CLIENT_USAGE_GUIDE.md @@ -0,0 +1,125 @@ +# Client Usage Guide + +Forge Client contains the Arma client-side addons that open player interfaces, +handle browser events, cache client-visible state, and forward authoritative +requests to the server addons. + +Use this guide as the entry point for client-side integration. Domain data, +validation, persistence, rewards, ownership, and checkout behavior remain +server-side responsibilities. + +## Client Responsibilities + +- Open Arma displays and `CT_WEBBROWSER` controls. +- Load browser UI assets from each addon's `ui/_site` folder. +- Receive browser alerts through `JSDialog` handlers. +- Translate browser events into local actions or CBA server events. +- Cache display state in client repositories. +- Push server responses back into browser UIs with `ExecJS`. +- Provide local-only utility state where the feature is intentionally local. + +## Authoritative Boundaries + +Client repositories are view state. They are useful for rendering, local UI +decisions, and short-lived session behavior, but they should not be treated as +durable state. + +Authoritative state lives in: + +- server SQF addons for mission and player workflow ownership +- the `forge_server` extension for durable and hot-state domain logic +- SurrealDB where the extension persists durable domain records + +## Common Runtime Flow + +Most browser-backed client addons follow this shape: + +1. The addon creates a display, finds a browser control, and registers a + `JSDialog` event handler. +2. The browser loads an HTML entrypoint from `ui/_site`. +3. The browser sends JSON alerts with an `event` name and `data` payload. +4. `fnc_handleUIEvents.sqf` parses the alert and routes the event. +5. A bridge object or repository sends a CBA server event when server data is + needed. +6. Server responses are caught in `XEH_postInitClient.sqf`. +7. The bridge sends browser update events back through `ExecJS`. + +Browser alert payload: + +```json +{ + "event": "module::action", + "data": {} +} +``` + +## Open UI Entry Points + +| UI | Entry point | +| --- | --- | +| Actor menu | `call forge_client_actor_fnc_openUI;` | +| Bank | `call forge_client_bank_fnc_openUI;` | +| ATM | `[true] call forge_client_bank_fnc_openUI;` | +| CAD | `call forge_client_cad_fnc_openUI;` | +| Garage | `call forge_client_garage_fnc_openUI;` | +| Virtual garage | `call forge_client_garage_fnc_openVG;` | +| Organization portal | `call forge_client_org_fnc_openUI;` | +| Phone | `call forge_client_phone_fnc_openUI;` | +| Store | `call forge_client_store_fnc_openUI;` | + +Notifications are normally opened during client initialization and then updated +through the notification event/service. + +## Addon Guides + +- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) + +## Extension Calls + +Client addons should usually call server SQF events, not the `forge_server` +extension directly. The server addon owns validation context and converts the +request into extension commands. + +Example: + +```sqf +[SRPC(bank,requestDeposit), [getPlayerUID player, 100]] call CFUNC(serverEvent); +``` + +Direct extension calls from client code bypass server authorization boundaries +and should be avoided. + +## Browser Bridge Notes + +`forge_client_common_fnc_initWebUIBridge` provides reusable bridge and screen +objects for newer browser UIs. It queues outbound events until a browser screen +is ready, then delivers payloads through: + +```sqf +_control ctrlWebBrowserAction ["ExecJS", format ["ForgeBridge.receive(%1)", _json]]; +``` + +Feature addons still own their event names, request payloads, and response +mapping. + +## Development Checklist + +- Keep feature-specific behavior in the owning addon. +- Send authoritative changes to the server addon. +- Use namespaced browser events such as `bank::deposit::request`. +- Treat `profileNamespace` as local player preference or utility state only. +- Make browser-ready events request the current server state before rendering + stale data. +- Queue or ignore bridge responses when the display is closed. +- Keep mission object setup on the mission/server side and client display logic + on the client side. diff --git a/arma/forge_pmc_simulator.Tanoa/guides/DEVELOPMENT_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..ce7f54e --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/DEVELOPMENT_GUIDE.md @@ -0,0 +1,134 @@ +# Development Guide + +This guide covers the usual path for adding or changing a Forge module. + +## Local Checks + +Before running storage-backed workflows locally, complete +[SurrealDB Setup](./surrealdb-setup.md). A local or dedicated server launch must +have SurrealDB running and a `config.toml` beside `forge_server_x64.dll` that +matches the running database. + +Run these before pushing Rust or extension changes: + +```powershell +cargo fmt --check +cargo check +cargo test +cargo build +cargo clippy --all-targets --all-features -- -D warnings +``` + +Run this after changing browser UI sources: + +```powershell +npm run build:webui +``` + +Build Arma packages with: + +```powershell +.\build-arma.ps1 +``` + +## Module Boundaries + +Keep each layer responsible for one kind of work: + +| Layer | Owns | Avoid | +| --- | --- | --- | +| `lib/models` | Data structures, serde defaults, validation helpers. | Database calls, SQF-specific context. | +| `lib/repositories` | Repository traits and in-memory stores. | SurrealDB-specific code. | +| `lib/services` | Business rules, workflow orchestration, structured results. | Arma engine calls, extension transport details. | +| `arma/server/extension` | Command parsing, context resolution, SurrealDB implementations, serialization to SQF. | Business rules that belong in services. | +| `arma/server/addons` | Server SQF lifecycle, game-object integration, calls into `forge_server`. | Direct database logic. | +| `arma/client/addons` | Client UI, keybinds, local UI events. | Authoritative persistence. | + +## Adding a Domain Module + +1. Add the model in `lib/models/src/.rs`. +2. Export the model from `lib/models/src/lib.rs`. +3. Add repository traits in `lib/repositories/src/.rs`. +4. Add in-memory repository support if the service needs tests or hot state. +5. Export the traits from `lib/repositories/src/lib.rs`. +6. Add service logic in `lib/services/src/.rs`. +7. Add focused unit tests for service behavior. +8. Export the service from `lib/services/src/lib.rs`. +9. Add a SurrealDB schema module under `arma/server/extension/src/schema`. +10. Add the concrete storage adapter under `arma/server/extension/src/storage`. +11. Register the storage adapter in `arma/server/extension/src/storage.rs`. +12. Add an extension command group under `arma/server/extension/src/.rs`. +13. Register the command group in `arma/server/extension/src/lib.rs`. +14. Add server addon functions under `arma/server/addons/` if SQF needs a module-level API. +15. Add client addon or browser UI files under `arma/client/addons/` if the module has player-facing UI. +16. Add documentation in `docs/` and module-level READMEs. + +## Extension Command Rules + +Commands should return one of these forms: + +- JSON string for structured results. +- `"true"` or `"false"` for simple existence and boolean operations. +- `"OK"` for successful destructive operations with no response body. +- `"Error: "` for failures. + +Prefer stable JSON shapes over ad hoc strings. SQF callers should always check +for the `"Error:"` prefix before parsing JSON. + +Example: + +```sqf +private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]]; +private _payload = _result select 0; + +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Actor request failed: %1", _payload]; +}; + +private _actor = fromJSON _payload; +``` + +## Persistence Rules + +SurrealDB is the durable store. Keep database-specific mapping in the extension +storage adapters, not in services or repository traits. + +When changing persisted data: + +- Update or add the matching `.surql` schema module. +- Update the concrete storage adapter. +- Preserve existing records when possible through serde defaults or migration + logic. +- Add tests at the service level for behavior, and add storage tests only when + database mapping is the risk. + +## Hot-State Rules + +Use hot state for data that is read or mutated frequently during a player +session. Hot-state modules usually provide: + +- `init` to load durable state into memory. +- `get` to read the runtime copy. +- `override` or focused mutation commands to update the runtime copy. +- `save` to write the runtime copy back to SurrealDB. +- `remove` to evict the runtime copy. + +Do not assume hot state is durable until `save` succeeds. + +## Web UI Rules + +Browser UI source files live under each client addon. Built assets usually land +under that addon's `ui/_site` directory. + +Use the existing common bridge in `arma/client/addons/common` when a UI needs +to send events back to SQF. Keep UI state and rendering in JavaScript, and keep +server-authoritative decisions in server SQF or Rust services. + +## Documentation Checklist + +When adding or changing a module, update: + +- `docs/MODULE_REFERENCE.md` for framework-level inventory. +- A module-specific README in the addon directory when SQF or UI usage changes. +- `arma/server/docs/api-reference.md` when extension commands change. +- Existing usage guides when payload shapes or workflows change. diff --git a/arma/forge_pmc_simulator.Tanoa/guides/ECONOMY_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/ECONOMY_USAGE_GUIDE.md new file mode 100644 index 0000000..eefa8cc --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/ECONOMY_USAGE_GUIDE.md @@ -0,0 +1,95 @@ +# Economy Usage Guide + +The economy server addon owns Arma-world service behavior for fuel, medical, +and repair interactions. It does not own money state. Money mutations go +through extension-backed bank and organization hot state before the world +effect is applied. + +## Dependencies + +- `forge_server_common` for logging, formatting, and player lookup. +- `forge_server_bank` for personal medical billing. +- `forge_server_org` for organization-funded services and medical fallback + debt. +- `forge_client_actor` and `forge_client_notifications` for targeted client + responses. + +## Fuel + +Fuel is organization-funded. + +When refueling stops, `fnc_initFEconomyStore.sqf` calculates the fuel delta and +cost, charges the player's organization through `OrgStore chargeCheckout`, and +syncs the organization patch to online members. If organization funds cannot +cover the refuel, the vehicle is rolled back to the fuel level it had when the +session started. + +Garage UI refuel requests use the server `RefuelService` event. The fuel store +calculates missing fuel from the vehicle config `fuelCapacity`, charges the +player's organization, and fills the vehicle only after the organization charge +succeeds. + +## Repair + +Repair is organization-funded. + +Use the repair service event: + +```sqf +[QEGVAR(economy,RepairService), [_target, _unit, _cost]] call CBA_fnc_serverEvent; +``` + +`_cost` is optional. Passing `-1` uses the configured service repair cost. +The target is only repaired after the organization charge succeeds. + +The client garage UI forwards selected nearby vehicle repair requests through +the same event. + +## Rearm + +Rearm is organization-funded. + +Use the rearm service event: + +```sqf +[QEGVAR(economy,RearmService), [_target, _unit, _cost]] call CBA_fnc_serverEvent; +``` + +`_cost` is optional. Passing `-1` uses the configured service rearm cost. +The target is only rearmed after the organization charge succeeds. +`setVehicleAmmo` has global effects, but the ammo is only added to local +turrets, so the service broadcasts the ammo reset after billing succeeds. + +The client garage UI forwards selected nearby vehicle rearm requests through +the same event. + +## Medical + +Medical is player-funded first. + +When a heal is requested, `fnc_initMEconomyStore.sqf` uses this billing order: + +1. Charge the player's bank balance when it can cover the medical fee. +2. Otherwise charge the player's cash when it can cover the fee. +3. If neither personal balance can cover the fee, charge organization funds. +4. When organization funds cover the fallback charge, record the same amount as + debt on the player's organization credit line. + +The heal only completes after one of those charges succeeds. If personal +billing is unavailable, the heal does not fall back to organization funds +because the server cannot verify that the player is unable to cover the fee. + +## Medical Debt Repayment + +Medical fallback debt uses the existing organization credit-line repayment +flow. The organization treasury is reduced when the service is rendered, and +the player's credit-line `amount_due` increases by the medical fee. When the +player repays through the bank credit-line repayment action, player bank funds +are moved back into the organization treasury. + +## Hot-Cache Boundary + +The economy addon should stay server-authoritative for world effects such as +vehicle fuel, vehicle repair, healing, respawn placement, and death inventory +movement. Bank and organization balances should continue to mutate through the +extension-backed hot-cache services. diff --git a/arma/forge_pmc_simulator.Tanoa/guides/FRAMEWORK_ARCHITECTURE.md b/arma/forge_pmc_simulator.Tanoa/guides/FRAMEWORK_ARCHITECTURE.md new file mode 100644 index 0000000..08f4529 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/FRAMEWORK_ARCHITECTURE.md @@ -0,0 +1,145 @@ +# Framework Architecture + +Forge is organized around domain modules. A domain usually has SQF addon +entry points, Rust models, repository traits, service logic, extension command +handlers, and optional browser UI. + +## Runtime Flow + +![Architectural Flow Diagram](architecture-flow.svg) + +```text +Arma client UI or SQF action + -> client addon bridge + -> server addon function + -> forge_server callExtension command + -> extension command group + -> forge-services domain service + -> forge-repositories trait + -> SurrealDB repository implementation + -> SurrealDB +``` + +For small payloads, server SQF calls `forge_server` directly through the +extension bridge. For large payloads, `arma/server/addons/extension` stages +request and response chunks through the extension transport module. + +## Main Layers + +### Client Addons + +Client addons live under `arma/client/addons`. They own local player UX, +keybinds, browser UI dialogs, and UI-to-SQF event handling. When a client needs +durable or authoritative state, it routes work to the matching server addon +instead of touching persistence directly. + +### Server Addons + +Server addons live under `arma/server/addons`. They own server-side SQF +initialization, game-object integration, validation near the Arma runtime, and +calls into the Rust extension. The `extension` addon is the shared bridge for +`callExtension` and transport handling. + +### Rust Extension + +The server extension lives under `arma/server/extension`. It registers the +`forge_server` command groups, loads configuration, initializes SurrealDB, and +maps SQF command inputs into service calls. + +The extension should stay thin: + +- Parse and validate command arguments that arrive from SQF. +- Resolve Arma-specific context such as player UID when required. +- Call the matching service. +- Serialize the service result back to JSON or a simple string. + +### Shared Rust Crates + +The `lib` workspace contains reusable Rust crates: + +- `forge-models`: shared domain structs and serialization rules. +- `forge-repositories`: storage-agnostic repository traits and in-memory + implementations used by tests and hot-state services. +- `forge-services`: domain behavior, validation, and mutation workflows. +- `forge-shared`: cross-crate helpers. + +### Persistence + +Durable storage is SurrealDB. Schema modules live under +`arma/server/extension/src/schema`, and concrete SurrealDB repository +implementations live under `arma/server/extension/src/storage`. + +Repository traits stay in `lib/repositories` so service logic remains testable +without a database. + +## Hot State + +Several domains have `hot` command groups. Hot state keeps a runtime copy of +frequently accessed data in memory, then saves it back to durable storage when +requested. This is useful for player state that changes often during a session. + +Typical hot-state flow: + +```text +actor:hot:init +actor:hot:get +actor:hot:override +actor:hot:save +actor:hot:remove +``` + +Use hot state for session workflows. Use normal domain commands for direct +durable CRUD operations. + +## Transport Layer + +The transport layer exists because Arma extension calls have practical payload +size limits. It provides chunked request and response handling while still +routing to the same domain command groups. + +Common direct command: + +```sqf +"forge_server" callExtension ["status", []]; +``` + +Common transport path: + +```text +server addon fnc_extCall + -> transport:request:append + -> transport:invoke_stored + -> transport:response:get +``` + +## Configuration + +The server extension reads `config.toml` next to the extension DLL. The current +persistence section is: + +```toml +[surreal] +endpoint = "127.0.0.1:8000" +namespace = "forge" +database = "main" +username = "root" +password = "root" +connect_timeout_ms = 5000 +``` + +`config.toml` is a launch prerequisite for server owners and developers. The +file must exist beside `forge_server_x64.dll`, and SurrealDB must already be +running at the configured endpoint before starting a Forge-enabled dedicated +server or local multiplayer test. Clients and mission designers do not run this +configuration unless they are hosting locally, but the server they connect to +must have it in place. + +For install links and role-based setup guidance, see +[SurrealDB Setup](./surrealdb-setup.md). + +Check persistence readiness before issuing commands that require storage: + +```sqf +"forge_server" callExtension ["status", []]; +"forge_server" callExtension ["surreal:status", []]; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/GARAGE_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/GARAGE_USAGE_GUIDE.md new file mode 100644 index 0000000..de9ef66 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/GARAGE_USAGE_GUIDE.md @@ -0,0 +1,212 @@ +# Garage Usage Guide + +The garage module stores physical player vehicles. Each record keeps the +vehicle classname, generated plate UUID, fuel, overall damage, and detailed hit +point damage. + +## Storage Model + +Garage data is persisted through SurrealDB by the server extension. + +```json +{ + "plate-uuid": { + "plate": "plate-uuid", + "classname": "B_Quadbike_01_F", + "fuel": 1.0, + "damage": 0.0, + "hit_points": { + "names": ["hitengine"], + "selections": ["engine_hitpoint"], + "values": [0.0] + } + } +} +``` + +Rules validated by the Rust service: + +- A player garage can contain up to 5 vehicles. +- `garage:add` generates a UUID plate automatically. +- `fuel`, `damage`, and every hit point value must be between `0.0` and `1.0`. +- `hit_points.names`, `hit_points.selections`, and `hit_points.values` must have + the same length. +- `garage:get`, `garage:patch`, and `garage:remove` require an existing garage. +- `garage:add` creates an empty garage automatically when one does not exist. + +## Commands + +All commands are called on the `garage` group. + +| Command | Arguments | Returns | +| --- | --- | --- | +| `garage:create` | `uid` | Empty vehicle map as JSON. | +| `garage:get` | `uid` | Vehicle map as JSON. | +| `garage:add` | `uid`, `vehicle_json` | Updated vehicle map as JSON. | +| `garage:update` | `uid`, `vehicles_json` | Replaced vehicle map as JSON. | +| `garage:patch` | `uid`, `patch_json` | Updated vehicle map as JSON. | +| `garage:remove` | `uid`, `remove_json` | Updated vehicle map as JSON. | +| `garage:delete` | `uid` | `OK`. | +| `garage:exists` | `uid` | `true` or `false`. | + +## Error Handling + +Every command returns a string payload. Always check for the `Error:` prefix +before parsing JSON. + +```sqf +private _result = "forge_server" callExtension ["garage:get", [getPlayerUID player]]; +private _payload = _result select 0; + +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Garage error: %1", _payload]; +}; + +private _garage = fromJSON _payload; +``` + +## Add a Vehicle + +`garage:add` requires `classname`, `fuel`, `damage`, and `hit_points`. + +```sqf +private _hitPointData = getAllHitPointsDamage _vehicle; +private _hitPoints = createHashMapFromArray [ + ["names", _hitPointData select 0], + ["selections", _hitPointData select 1], + ["values", _hitPointData select 2] +]; + +private _vehicleData = createHashMapFromArray [ + ["classname", typeOf _vehicle], + ["fuel", fuel _vehicle], + ["damage", damage _vehicle], + ["hit_points", _hitPoints] +]; + +private _result = "forge_server" callExtension ["garage:add", [ + getPlayerUID player, + toJSON _vehicleData +]]; + +private _payload = _result select 0; +if (_payload find "Error:" == 0) exitWith { + hint format ["Failed to store vehicle: %1", _payload]; +}; + +private _garage = fromJSON _payload; +``` + +The returned value is a hash map keyed by generated plate. To find the newly +stored vehicle, compare returned keys before and after the add, or search by +classname if your workflow guarantees a unique pending vehicle. + +```sqf +private _storedPlate = ""; +{ + private _vehicleRecord = _garage get _x; + if ((_vehicleRecord get "classname") == typeOf _vehicle) then { + _storedPlate = _x; + }; +} forEach keys _garage; +``` + +## Patch a Vehicle + +`garage:patch` updates selected fields for one plate. The `plate` field is +required. `fuel`, `damage`, and `hit_points` are optional. + +```sqf +private _patch = createHashMapFromArray [ + ["plate", _vehicle getVariable ["forge_garage_plate", ""]], + ["fuel", fuel _vehicle], + ["damage", damage _vehicle] +]; + +private _result = "forge_server" callExtension ["garage:patch", [ + getPlayerUID player, + toJSON _patch +]]; +``` + +## Remove a Vehicle + +`garage:remove` expects JSON with a `plate` field. + +```sqf +private _remove = createHashMapFromArray [ + ["plate", _plate] +]; + +private _result = "forge_server" callExtension ["garage:remove", [ + getPlayerUID player, + toJSON _remove +]]; +``` + +## Spawn a Stored Vehicle + +```sqf +fnc_spawnGarageVehicle = { + params ["_plate"]; + + private _result = "forge_server" callExtension ["garage:get", [getPlayerUID player]]; + private _payload = _result select 0; + + if (_payload find "Error:" == 0) exitWith { + hint format ["Failed to load garage: %1", _payload]; + objNull + }; + + private _garage = fromJSON _payload; + private _vehicleData = _garage getOrDefault [_plate, createHashMap]; + if (_vehicleData isEqualTo createHashMap) exitWith { + hint "Vehicle plate was not found in your garage."; + objNull + }; + + private _vehicle = (_vehicleData get "classname") createVehicle (player getPos [10, getDir player]); + _vehicle setFuel (_vehicleData getOrDefault ["fuel", 1]); + _vehicle setDamage (_vehicleData getOrDefault ["damage", 0]); + _vehicle setVariable ["forge_garage_plate", _plate, true]; + + private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap]; + private _names = _hitPoints getOrDefault ["names", []]; + private _values = _hitPoints getOrDefault ["values", []]; + + { + _vehicle setHitPointDamage [_x, _values select _forEachIndex]; + } forEach _names; + + private _remove = createHashMapFromArray [["plate", _plate]]; + "forge_server" callExtension ["garage:remove", [getPlayerUID player, toJSON _remove]]; + + _vehicle +}; +``` + +## Hot State + +The `garage:hot:*` commands keep a runtime copy of a player's garage and write +it back only when `garage:hot:save` runs. + +| Command | Arguments | Returns | +| --- | --- | --- | +| `garage:hot:init` | `uid` | Vehicle map as JSON. | +| `garage:hot:get` | `uid` | Vehicle map as JSON. | +| `garage:hot:override` | `uid`, `vehicles_json` | Vehicle map as JSON. | +| `garage:hot:add` | `uid`, `vehicle_json` | Vehicle map as JSON. | +| `garage:hot:remove_vehicle` | `uid`, `remove_json` | Vehicle map as JSON. | +| `garage:hot:save` | `uid` | Current hot vehicle map as JSON. | +| `garage:hot:remove` | `uid` | `OK`. | + +Use hot state for session-heavy vehicle workflows. Use the durable commands for +simple store/retrieve operations. + +## Best Practices + +- Store the generated plate on spawned vehicles with `setVariable`. +- Use `garage:patch` for frequent fuel and damage syncs. +- Use `garage:update` only when replacing the whole vehicle map intentionally. +- Do not delete the world vehicle until `garage:add` succeeds. +- Treat vehicle maps as hash maps keyed by plate, not arrays. diff --git a/arma/forge_pmc_simulator.Tanoa/guides/ICOM_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/ICOM_USAGE_GUIDE.md new file mode 100644 index 0000000..36a3bb0 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/ICOM_USAGE_GUIDE.md @@ -0,0 +1,195 @@ +# ICOM Usage Guide + +ICOM is the Forge inter-server communication helper. It lets multiple Arma 3 +servers exchange generic JSON events through a central TCP hub instead of +connecting directly to each other. + +## Runtime Shape + +```text +Arma server SQF + -> forge_server extension icom:* command + -> ICOM client inside the extension + -> forge-icom TCP hub + -> target server extension + -> forge_icom_event CBA server event +``` + +The ICOM hub lives in `bin/icom`. The Arma server extension integrates with it +through `arma/server/extension/src/icom.rs`. + +## Components + +| Component | Path | Role | +| --- | --- | --- | +| ICOM hub binary | `bin/icom` | Standalone TCP router for connected servers. | +| ICOM client library | `bin/icom/src/client.rs` | Rust client used by the Forge server extension and examples. | +| Extension command group | `arma/server/extension/src/icom.rs` | Exposes `icom:*` commands to SQF and forwards inbound events to Arma. | +| SQF callback bridge | `arma/server/addons/main/XEH_preInit.sqf` | Receives extension callbacks and re-emits `forge_icom_event` through CBA. | + +## Build and Run the Hub + +Build the release binary: + +```powershell +cargo build --release -p forge-icom +``` + +Run it during development: + +```powershell +cargo run -p forge-icom +``` + +The default bind address is `0.0.0.0:9090`. + +## Hub Configuration + +Copy `bin/icom/config.example.toml` to `config.toml` beside the `forge-icom` +executable or into the working directory used to launch it. + +```toml +[server] +host = "0.0.0.0" +port = 9090 +``` + +Use `127.0.0.1` for same-machine testing. Use `0.0.0.0` when remote Arma +servers need to connect, and secure the port at the firewall or host network +layer. + +## Extension Commands + +ICOM commands are exposed through the `icom` command group in `forge_server`. + +| Command | Arguments | Returns | +| --- | --- | --- | +| `icom:connect` | `address`, `server_id` | `Connection initiated` or `ERROR: Already connected`. | +| `icom:send_event` | `target_server`, `event_name`, `data_json` | `OK` or `ERROR: `. | +| `icom:broadcast` | `event_name`, `data_json` | `OK` or `ERROR: `. | + +The current extension connects when `icom:connect` is called. Start the ICOM hub +first, then connect each Arma server with a unique `server_id`. + +```sqf +private _result = "forge_server" callExtension [ + "icom:connect", + ["127.0.0.1:9090", "server_1"] +]; +diag_log format ["[ICOM] Connect result: %1", _result select 0]; +``` + +## Send an Event + +Send a targeted event to one connected server: + +```sqf +private _data = createHashMapFromArray [ + ["coords", [1234, 5678, 0]], + ["supplies", ["ammo_box", "medical_supplies"]] +]; + +"forge_server" callExtension [ + "icom:send_event", + ["server_2", "supply_drop", toJSON _data] +]; +``` + +Broadcast to every connected server except the sender: + +```sqf +private _alert = createHashMapFromArray [ + ["message", "Server restart in 5 minutes"], + ["severity", "warning"] +]; + +"forge_server" callExtension [ + "icom:broadcast", + ["global_alert", toJSON _alert] +]; +``` + +## Receive Events + +Inbound ICOM events are forwarded to SQF as the CBA server event +`forge_icom_event`. + +```sqf +["forge_icom_event", { + params ["_eventName", "_data"]; + + switch (_eventName) do { + case "supply_drop": { + private _coords = _data getOrDefault ["coords", []]; + private _supplies = _data getOrDefault ["supplies", []]; + diag_log format ["[ICOM] Supply drop at %1: %2", _coords, _supplies]; + }; + case "global_alert": { + private _message = _data getOrDefault ["message", ""]; + if (_message isNotEqualTo "") then { + [_message] remoteExec ["hint", 0]; + }; + }; + default { + diag_log format ["[ICOM] Unhandled event: %1 | %2", _eventName, _data]; + }; + }; +}] call CBA_fnc_addEventHandler; +``` + +## Message Protocol + +The hub uses newline-delimited JSON. The first message from each client is a +registration payload: + +```json +{ + "type": "register", + "server_id": "server_1" +} +``` + +Targeted events use `type: "event"`: + +```json +{ + "type": "event", + "target_server": "server_2", + "event_name": "supply_drop", + "data": { + "coords": [1234, 5678, 0] + } +} +``` + +Broadcasts use `type: "broadcast"` and are routed to all connected servers +except the sender. + +## Operational Notes + +- Server IDs must be unique. If the same ID reconnects, the hub replaces the old + connection. +- Event names are mission/application contracts. ICOM only routes them; it does + not validate gameplay meaning. +- Always send valid JSON in the `data_json` argument. +- `icom:send_event` and `icom:broadcast` return quickly after scheduling async + work in the extension. Check extension and ICOM hub logs for delivery errors. +- Keep event payloads small and stable. Use IDs or compact data where possible. + +## Testing + +Start the hub: + +```powershell +cargo run -p forge-icom +``` + +Run example clients in separate terminals: + +```powershell +cargo run -p forge-icom --example server_1_client +cargo run -p forge-icom --example server_2_client +``` + +For Arma testing, start the hub, connect the server with `icom:connect`, register +a `forge_icom_event` handler, then send an event from another connected server. diff --git a/arma/forge_pmc_simulator.Tanoa/guides/LOCKER_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/LOCKER_USAGE_GUIDE.md new file mode 100644 index 0000000..4424596 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/LOCKER_USAGE_GUIDE.md @@ -0,0 +1,203 @@ +# Locker Usage Guide + +The locker module stores physical player inventory items by classname. It is +separate from the virtual arsenal unlock module documented in +[Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md). + +## Storage Model + +Locker data is persisted through SurrealDB by the server extension. + +```json +{ + "arifle_MX_F": { + "category": "weapon", + "classname": "arifle_MX_F", + "amount": 1 + } +} +``` + +Rules validated by the Rust service: + +- A locker can contain up to 25 unique classnames. +- `category` and `classname` cannot be empty. +- `amount` must be greater than `0`. +- `locker:add` creates an empty locker automatically when one does not exist. +- `locker:get`, `locker:patch`, and `locker:remove` require an existing locker. +- `locker:remove` takes the classname directly, not a JSON object. + +## Commands + +All commands are called on the `locker` group. + +| Command | Arguments | Returns | +| --- | --- | --- | +| `locker:create` | `uid` | Empty item map as JSON. | +| `locker:get` | `uid` | Item map as JSON. | +| `locker:add` | `uid`, `item_json` | Updated item map as JSON. | +| `locker:update` | `uid`, `items_json` | Replaced item map as JSON. | +| `locker:patch` | `uid`, `patch_json` | Updated item map as JSON. | +| `locker:remove` | `uid`, `classname` | Updated item map as JSON. | +| `locker:delete` | `uid` | `OK`. | +| `locker:exists` | `uid` | `true` or `false`. | + +## Error Handling + +Every command returns a string payload. Always check for the `Error:` prefix +before parsing JSON. + +```sqf +private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]]; +private _payload = _result select 0; + +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Locker error: %1", _payload]; +}; + +private _locker = fromJSON _payload; +``` + +## Add an Item + +`locker:add` creates or overwrites one classname entry. + +```sqf +private _item = createHashMapFromArray [ + ["category", "weapon"], + ["classname", "arifle_MX_F"], + ["amount", 1] +]; + +private _result = "forge_server" callExtension ["locker:add", [ + getPlayerUID player, + toJSON _item +]]; + +private _payload = _result select 0; +if (_payload find "Error:" == 0) exitWith { + hint format ["Failed to store item: %1", _payload]; +}; + +private _locker = fromJSON _payload; +``` + +## Patch an Amount + +`locker:patch` currently patches the `amount` field for an existing classname. + +```sqf +private _patch = createHashMapFromArray [ + ["classname", "arifle_MX_F"], + ["amount", 5] +]; + +private _result = "forge_server" callExtension ["locker:patch", [ + getPlayerUID player, + toJSON _patch +]]; +``` + +## Remove an Item + +`locker:remove` takes the classname as the second argument. + +```sqf +private _result = "forge_server" callExtension ["locker:remove", [ + getPlayerUID player, + "arifle_MX_F" +]]; +``` + +## Retrieve an Item + +```sqf +fnc_retrieveLockerItem = { + params ["_classname"]; + + private _result = "forge_server" callExtension ["locker:get", [getPlayerUID player]]; + private _payload = _result select 0; + + if (_payload find "Error:" == 0) exitWith { + hint format ["Failed to load locker: %1", _payload]; + false + }; + + private _locker = fromJSON _payload; + private _item = _locker getOrDefault [_classname, createHashMap]; + if (_item isEqualTo createHashMap) exitWith { + hint "Item was not found in your locker."; + false + }; + + private _amount = _item getOrDefault ["amount", 0]; + if (_amount <= 0) exitWith { + hint "Item is out of stock."; + false + }; + + if !(player canAdd _classname) exitWith { + hint "Not enough inventory space."; + false + }; + + player addItem _classname; + + if (_amount > 1) then { + private _patch = createHashMapFromArray [ + ["classname", _classname], + ["amount", _amount - 1] + ]; + "forge_server" callExtension ["locker:patch", [getPlayerUID player, toJSON _patch]]; + } else { + "forge_server" callExtension ["locker:remove", [getPlayerUID player, _classname]]; + }; + + true +}; +``` + +## Replace the Whole Locker + +`locker:update` replaces the whole item map. Use it for explicit bulk syncs, +not single-item changes. + +```sqf +private _items = createHashMapFromArray [ + ["arifle_MX_F", createHashMapFromArray [ + ["category", "weapon"], + ["classname", "arifle_MX_F"], + ["amount", 1] + ]] +]; + +private _result = "forge_server" callExtension ["locker:update", [ + getPlayerUID player, + toJSON _items +]]; +``` + +## Hot State + +The `locker:hot:*` commands keep a runtime copy of a player's locker and write +it back only when `locker:hot:save` runs. + +| Command | Arguments | Returns | +| --- | --- | --- | +| `locker:hot:init` | `uid` | Item map as JSON. | +| `locker:hot:get` | `uid` | Item map as JSON. | +| `locker:hot:override` | `uid`, `items_json` | Item map as JSON. | +| `locker:hot:save` | `uid` | Current hot item map as JSON. | +| `locker:hot:remove` | `uid` | `OK`. | + +Use hot state for session-heavy locker workflows. Use the durable commands for +simple item deposits and withdrawals. + +## Best Practices + +- Keep categories normalized, for example `weapon`, `magazine`, `item`, or + `backpack`. +- Use `locker:patch` for quantity changes. +- Use `locker:remove` when quantity reaches zero. +- Treat the locker response as a hash map keyed by classname. +- Check capacity before bulk operations that may exceed 25 unique items. diff --git a/arma/forge_pmc_simulator.Tanoa/guides/MISSION_DESIGNER_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/MISSION_DESIGNER_GUIDE.md new file mode 100644 index 0000000..f1a998d --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/MISSION_DESIGNER_GUIDE.md @@ -0,0 +1,750 @@ +# Mission Designer Guide + +Build playable Forge missions in Eden with the required interaction objects, +garage markers, and CAD-compatible task modules. + +This guide focuses on editor placement and mission validation. Framework +internals, extension commands, and persistence details are covered in the +developer-oriented module guides. + +## Core Rule + +Most Forge systems become available to players through nearby Eden objects. +Place the object, give it the correct variable name in Eden, and the server +initializer marks it with the runtime variable the actor menu scans for. + +Players must be within 5 meters of the object for the actor menu to offer the +action. + +## Interaction Objects + +Use the object's Eden variable name, not its display name. The matching is +case-sensitive in some initializers, so use lower-case names. + +![Bank object variable name field](images/eden/bank_obj_var.jpg) + +| System | Eden Object Variable Name | Runtime Variable | Player Action | Notes | +| --- | --- | --- | --- | --- | +| Bank | name contains `bank` | `isBank = true` | Full bank UI | Allows full banking workflows, including PIN changes. | +| ATM | name contains `atm` | `isAtm = true` | ATM bank UI | ATM mode requires PIN authorization and does not allow PIN changes. | +| Store | name contains `store` | `isStore = true` | Store UI | Store catalog and checkout behavior are configured server-side. | +| Garage | name contains `garage` | `isGarage = true` | Garage UI and virtual garage | Include a garage category in the name or set `garageType` manually. | +| Locker | name contains `locker` | local `isLocker = true` | Virtual arsenal action | The server hides the editor object; each client creates a local locker at the same position. | + +Recommended object names: + +```text +atm +bank +store +locker +garage_hq +garage_hq_2 +``` + +The example mission uses short lower-case names. Keep single-use objects simple, +add an index when there may be multiple copies, and include a site label for +garage objects so related spawn markers can share the same prefix. + +Avoid using `forge_locker_box` as an editor-placed locker variable name. That +name is reserved by the client-side virtual arsenal box. + +## Manual Object Variables + +The automatic initializers are the normal path. If a mission script creates +interaction objects dynamically, set the same variables manually: + +```sqf +_bankLaptop setVariable ["isBank", true, true]; +_atmTerminal setVariable ["isAtm", true, true]; +_storeCounter setVariable ["isStore", true, true]; +_garageTerminal setVariable ["isGarage", true, true]; +_garageTerminal setVariable ["garageType", "cars", true]; +``` + +Supported garage types are: + +- `cars` +- `armor` +- `helis` +- `planes` +- `naval` +- `other` + +## Garage Markers + +Garage interaction objects open the garage UI. Vehicle spawn positions come +from Eden markers. + +![Garage object placement](images/eden/garage_obj.jpg) + +![Garage object variable name](images/eden/garage_obj_var.jpg) + +![Garage category spawn markers](images/eden/garage_spawn_mrkrs.jpg) + +![Garage spawn marker variable name](images/eden/garage_spawn_1_mrkr_var.jpg) + +Additional garage sites use the same pattern: place another garage interaction +object, give it a `garage` variable name that identifies the site, then place +matching category spawn markers near that garage. + +![Second garage object placement](images/eden/garage_obj_2.jpg) + +![Second garage object variable name](images/eden/garage_obj_2_var.jpg) + +![Second garage site spawn markers](images/eden/garage_spawn_2_mrkrs.jpg) + +Create empty markers near each garage site. Marker names must contain `garage` +and one supported garage category: + +```text +garage_hq_cars +garage_hq_armor +garage_hq_helis +garage_hq_helis_1 +garage_hq_planes +garage_hq_naval +garage_hq_other +``` + +This convention keeps the site and category visible in the marker name: +`garage_hq_planes` is the planes spawn marker for `garage_hq`, while +`garage_hq_2` can use another nearby set of `garage_hq_*` category markers for +the second HQ garage area. If two garage objects of the same category are close +to each other, include the full object name in the marker prefix, such as +`garage_hq_2_planes`. + +Use these rules: + +1. Put the marker where the vehicle should spawn. +2. Rotate the marker to control spawn heading. +3. Keep the marker close to the matching garage object. +4. Include the garage object's variable name when multiple garages exist at + different sites. +5. Do not allow parked vehicles to block the marker. If a vehicle is within 5 + meters of the spawn position, the virtual garage blocks the session. + +Vehicle spawning is strict by category. A garage without a matching category +marker cannot spawn that vehicle category. + +## Store Setup + +Store objects only unlock the store UI. The actual item catalog, prices, +payment source handling, locker grants, and garage unlocks are server-owned. + +![Store object placement](images/eden/store_obj.jpg) + +![Store object variable name](images/eden/store_obj_var.jpg) + +Minimum Eden setup: + +1. Place a terminal, table, NPC, or other object players can stand near. +2. Set its Eden variable name to something containing `store`. +3. Test that the actor menu shows the store action within 5 meters. + +## Bank and ATM Setup + +Bank and ATM objects intentionally expose different workflows. + +![Bank object placement](images/eden/bank_obj.jpg) + +![Bank object variable name](images/eden/bank_obj_var.jpg) + +![ATM object placement](images/eden/atm_obj.jpg) + +![ATM object variable name](images/eden/atm_obj_var.jpg) + +Use a `bank` object for the full bank interface: + +- account view +- transfers +- earnings deposit +- PIN change + +Use an `atm` object for ATM access: + +- PIN-gated account access +- ATM-mode banking actions +- no PIN change + +Minimum Eden setup: + +1. Place one or more bank laptops or terminals with variable names containing + `bank`. +2. Place one or more ATM objects with variable names containing `atm`. +3. Keep the object accessible so players can stand within 5 meters. + +## Locker Setup + +Locker objects are slightly different from other interaction objects. The +server finds editor-placed objects whose variable names contain `locker`, hides +those global objects, and each client creates a local locker object at the same +position. + +![Locker object placement](images/eden/locker_obj.jpg) + +![Locker object variable name](images/eden/locker_obj_var.jpg) + +Minimum Eden setup: + +1. Place a container object where the locker should appear. +2. Set its Eden variable name to something containing `locker`. +3. Do not use `forge_locker_box`. +4. Test that the local locker appears and opens the virtual arsenal action. + +## Medical Spawn Setup + +The medical economy store discovers up to eleven medical spawn objects by exact +mission namespace variable name: + +- `med_spawn` +- `med_spawn_1` +- `med_spawn_2` +- continuing through `med_spawn_10` + +These objects are used for medical respawn placement and occupancy checks. + +![Medical spawn object placement](images/eden/med_spawn_obj.jpg) + +![Medical spawn object variable name](images/eden/med_spawn_obj_var.jpg) + +Minimum Eden setup: + +1. Place an object at each medical respawn position. +2. Set the first object's Eden variable name to `med_spawn`. +3. Set additional medical spawns to `med_spawn_1`, `med_spawn_2`, and so on. +4. Keep each spawn position clear enough for a revived player to occupy. + +## CAD Access + +The CAD UI is currently opened from the actor menu action path, but there is no +server initializer that marks Eden objects as dedicated CAD terminals. If a +mission needs a CAD terminal object, wire it through mission script or a custom +interaction that calls: + +```sqf +[] spawn forge_client_cad_fnc_openUI; +``` + +Tasks show in CAD only when they are created through a CAD-compatible task +creation path. + +## CEO and Dispatch Slots + +Forge grants dispatch-board permissions from the player's Eden unit variable +name when that player belongs to the default organization. + +Use these exact lower-case variable names: + +| Slot | Eden Unit Variable Name | Permissions | +| --- | --- | --- | +| CEO | `ceo` | Can administer the default organization, use default organization funds where supported, and use the CAD dispatch board. | +| Dispatch | `dispatch` | Can use the CAD dispatch board. | + +![CEO unit placement](images/eden/ceo_unit.jpg) + +![CEO unit variable name](images/eden/ceo_unit_var.jpg) + +![Dispatch unit placement](images/eden/dispatch_unit.jpg) + +![Dispatch unit variable name](images/eden/dispatch_unit_var.jpg) + +The CEO slot is intentionally broader than the dispatch slot. Use it for the +player who should administrate the default organization. Use the dispatch slot +for players who need dispatcher tools without default organization +administration rights. + +## Task and CAD Setup + +Mission designers should use Forge Eden task modules for CAD-visible work. +Those modules delegate to `forge_task_fnc_startTask`, which creates the +BIS task, registers the Forge task catalog entry, sets active task state, and +dispatches the task handler. + +Use the Arma 3 `Create Task` module when you need a standard BIS map task +alongside Forge task handling. Use Forge task modules for CAD-visible task +contracts and runtime task logic. + +![Arma 3 Create Task module placement](images/eden/create_task_mod.jpg) + +![Arma 3 Create Task module parameters](images/eden/create_task_mod_params.jpg) + +![Attack task module placement](images/eden/attack_task_mod.jpg) + +![Attack task module parameters](images/eden/attack_task_mod_params.jpg) + +![Attack task target sync](images/eden/attack_task_tgts.jpg) + +![CAD visible task](images/eden/cad-visible-task.jpg) + +CAD-compatible task creation paths: + +| Path | CAD Compatible | Use When | +| --- | --- | --- | +| Forge Eden task modules | Yes | Normal mission-designer workflow. | +| `forge_task_fnc_startTask` | Yes | Scripted or generated mission content. | +| Dynamic mission manager attack tasks | Yes | Server-generated attack missions. | +| `forge_task_fnc_handler` directly | Only if catalog and BIS task already exist | Advanced scripted flows. | +| Direct task function calls | No by default | Custom server-owned flows that do not need CAD assignment. | + +General task rules: + +1. Give every task a unique `TaskID`. +2. Set success and fail limits explicitly. +3. Use area markers for zone fields. +4. Use Forge grouping modules where required. +5. Sync task modules to real world objects, units, vehicles, or grouping + modules. +6. To chain tasks, set `Prerequisite Task IDs` on the dependent task module to + a comma-separated list of task IDs that must succeed first. +7. Reward class fields use comma-separated class names without brackets, such + as `ItemGPS, FirstAidKit`. Existing SQF array strings such as + `["ItemGPS","FirstAidKit"]` still work for older missions. +8. Test that unchained tasks appear in CAD immediately and chained tasks appear + only after their prerequisite tasks succeed. + +Task chaining uses only task IDs. The dependent task is still registered during +mission setup, but it stays hidden from CAD, cannot be assigned, and does not +start its task logic until every prerequisite task has completed successfully. +If any prerequisite task fails or never completes, the dependent task remains +locked. + +Zone fields that must reference area markers: + +![Task marker fields](images/eden/create_task_mod_params.jpg) + +| Field | Used By | Marker Requirement | +| --- | --- | --- | +| `DefenseZone` | Defend Task | Rectangle or ellipse area marker. | +| `DeliveryZone` | Delivery Task | Rectangle or ellipse area marker. | +| `ExtZone` | Hostage and HVT capture tasks | Rectangle or ellipse area marker. | +| `CBRNZone` | Hostage CBRN variant | Rectangle or ellipse area marker. | + +## Task Module Setup Guides + +Use these task sections as the setup guide and capture plan. Save any new +screenshots under `docus/public/images/eden/` with the listed filenames. + +### Attack Task + +Use `FORGE_Module_Attack` when players need to eliminate hostile units or +vehicles. + +Existing screenshots: + +- `attack_task_mod.jpg` - Attack task module placement. +- `attack_task_mod_params.jpg` - Attack task module attributes. +- `attack_task_tgts.jpg` - Attack task synced to target units or vehicles. + +Setup: + +1. Place the enemy units or vehicles. +2. Place `FORGE_Module_Attack`. +3. Set a unique `TaskID`. +4. Set `LimitSuccess` to the number of targets that must be killed. +5. Set `LimitFail` if the mission should fail after too many losses. +6. Set reward funds, rating gain/loss, end-state behavior, and optional + `TimeLimit`. +7. Set `Prerequisite Task IDs` only if this attack task should unlock after + other tasks succeed. +8. Sync the attack module directly to the target units or vehicles. + +Validation: + +- The task appears in CAD after creation. +- Killing the configured number of targets succeeds the task. +- `TimeLimit` uses seconds; `0` disables the timer. + +### Destroy Task + +Use `FORGE_Module_Destroy` when players must destroy objects, vehicles, or +units. + +![Destroy task module placement](images/eden/destroy_task_mod.jpg) + +![Destroy task module parameters](images/eden/destroy_task_mod_params.jpg) + +![Destroy task target sync](images/eden/destroy_task_tgts.jpg) + +Setup: + +1. Place the objects, vehicles, or units that must be destroyed. +2. Place `FORGE_Module_Destroy`. +3. Set a unique `TaskID`. +4. Set `LimitSuccess` to the number of targets that must be destroyed. +5. Set `LimitFail` if the mission should fail after too many protected losses + or failed conditions. +6. Set reward funds, rating gain/loss, end-state behavior, and optional + `TimeLimit`. +7. Set `Prerequisite Task IDs` only if this destroy task should unlock after + other tasks succeed. +8. Sync the destroy module directly to the targets. + +Validation: + +- The module reads direct syncs only. +- Destroying the configured number of targets succeeds the task. +- `TimeLimit` uses seconds; `0` disables the timer. + +### Defuse Task + +Use `FORGE_Module_Defuse` when players must defuse explosives while optionally +protecting other entities. + +![Defuse task module placement](images/eden/defuse_task_mod.jpg) + +![Defuse task module parameters](images/eden/defuse_task_mod_params.jpg) + +![Explosive Entities grouping module](images/eden/defuse_explosives_mod.jpg) + +![Protected Entities grouping module](images/eden/defuse_protected_mod.jpg) + +The Defuse task screenshots show both module placement and the required sync +layout. + +Required module layout: + +```text +[Defuse Task] --> [Explosive Entities] --> explosive objects +[Defuse Task] --> [Protected Entities] --> protected objects, vehicles, or units +``` + +Setup: + +1. Place the explosive objects that players must defuse. +2. Place `FORGE_Module_Explosives`. +3. Sync each explosive object to `FORGE_Module_Explosives`. +4. Place any objects, vehicles, or units that must survive. +5. Place `FORGE_Module_Protected` when protected entities are part of the task. +6. Sync each protected entity to `FORGE_Module_Protected`. +7. Place `FORGE_Module_Defuse`. +8. Set a unique `TaskID`. +9. Set `LimitSuccess` to the number of explosives that must be defused. +10. Set `LimitFail` to the number of protected entities that can be lost before + failure. +11. Set `TimeLimit` to the IED countdown in seconds. +12. Set reward funds, rating gain/loss, and end-state behavior. +13. Set `Prerequisite Task IDs` only if this defuse task should unlock after + other tasks succeed. +14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`. +15. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected` if used. + +Validation: + +- The defuse task reads grouped entities, not direct object syncs. +- The ACE defuse event resolves the correct IED for the task. +- Defuse `TimeLimit` is the IED countdown and should be greater than `0`. + +### Delivery Task + +Use `FORGE_Module_Delivery` when players must move cargo objects into a +delivery zone. + +![Delivery task module placement](images/eden/delivery_task_mod.jpg) + +![Delivery task module parameters](images/eden/delivery_task_mod_params.jpg) + +![Cargo Entities grouping module](images/eden/delivery_cargo_mod.jpg) + +![Delivery area marker placement](images/eden/delivery_zone_mrkr.jpg) + +![Delivery marker name](images/eden/delivery_zone_mrkr_var.jpg) + +The Delivery task screenshots show both module placement and the required sync +layout. + +Required module layout: + +```text +[Delivery Task] --> [Cargo Entities] --> cargo objects +``` + +Setup: + +1. Place the cargo objects. +2. Create a rectangle or ellipse area marker for the delivery zone. +3. Place `FORGE_Module_Cargo`. +4. Sync each cargo object to `FORGE_Module_Cargo`. +5. Place `FORGE_Module_Delivery`. +6. Set a unique `TaskID`. +7. Set `DeliveryZone` to the delivery marker name. +8. Set `LimitSuccess` to the number of cargo objects that must arrive. +9. Set `LimitFail` to the number of cargo objects that can be damaged past the + fail threshold. +10. Set reward funds, rating gain/loss, end-state behavior, and optional + `TimeLimit`. +11. Set `Prerequisite Task IDs` only if this delivery task should unlock after + other tasks succeed. +12. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`. + +Validation: + +- `DeliveryZone` must be an area marker, not an icon marker. +- The runtime checks cargo with `inArea DeliveryZone`. +- The task succeeds only after the configured cargo count reaches the zone. + +### Hostage Task + +Use `FORGE_Module_Hostage` when players must rescue hostage units and move them +to an extraction zone. + +![Hostage task module placement](images/eden/hostage_task_mod.jpg) + +![Hostage task module parameters](images/eden/hostage_task_mod_params.jpg) + +![Hostage Entities grouping module](images/eden/hostage_entities_mod.jpg) + +![Shooter Entities grouping module](images/eden/hostage_shooters_mod.jpg) + +![Hostage extraction area marker placement](images/eden/hostage_ext_zone_mrkr.jpg) + +![Hostage extraction marker name](images/eden/hostage_ext_zone_mrkr_var.jpg) + +The Hostage task screenshots show both module placement and the required sync +layout. + +Required module layout: + +```text +[Hostage Task] --> [Hostage Entities] --> hostage units +[Hostage Task] --> [Shooter Entities] --> hostile shooter units +``` + +Setup: + +1. Place the hostage AI units. +2. Place the hostile shooter AI units. +3. Create a rectangle or ellipse area marker for the extraction zone. +4. If using the CBRN variant, create a rectangle or ellipse area marker for + `CBRNZone`. +5. Place `FORGE_Module_Hostages`. +6. Sync the hostage units to `FORGE_Module_Hostages`. +7. Place `FORGE_Module_Shooters`. +8. Sync the shooter units to `FORGE_Module_Shooters`. +9. Place `FORGE_Module_Hostage`. +10. Set a unique `TaskID`. +11. Set `ExtZone` to the extraction marker name. +12. Set `LimitSuccess` to the number of hostages that must be rescued. +13. Set `LimitFail` to the number of hostages that can be lost before failure. +14. Enable `CBRN Attack` or `Execution` when that mission variant is needed. +15. If `CBRN Attack` is enabled, set `CBRNZone`. +16. Set reward funds, rating gain/loss, end-state behavior, and optional + `TimeLimit`. +17. Set `Prerequisite Task IDs` only if this hostage task should unlock after + other tasks succeed. +18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`. +19. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`. + +Validation: + +- `ExtZone` and `CBRNZone` must be area markers. +- Hostage and shooter grouping modules should sync to real units only. +- The hostage timer waits until the assigned group leader acknowledges the + task. + +### HVT Task + +Use `FORGE_Module_HVT` when players must capture or eliminate high-value target +units. The `HVT Task` example below shows an elimination task. The `HVT Task 1` +example shows a capture/extract task. + +Eliminate HVT example: + +![HVT eliminate task module placement](images/eden/hvt_task_mod.jpg) + +![HVT eliminate task module parameters](images/eden/hvt_task_mod_params.jpg) + +Capture HVT example: + +![HVT capture task module placement](images/eden/hvt_capture_task_mod.jpg) + +![HVT capture task module parameters](images/eden/hvt_capture_task_mod_params.jpg) + +![HVT capture extraction area marker placement](images/eden/hvt_ext_zone_mrkr.jpg) + +![HVT capture extraction marker name](images/eden/hvt_ext_zone_mrkr_var.jpg) + +The HVT task screenshots show the direct HVT unit sync for both eliminate and +capture examples. + +Setup: + +1. Place the HVT unit or units. +2. Place `FORGE_Module_HVT`. +3. Set a unique `TaskID`. +4. For kill/eliminate missions, set `Capture HVT` to `False` and + `Eliminate HVT` to `True`. +5. For capture/extract missions, set `Capture HVT` to `True` and + `Eliminate HVT` to `False`. +6. If using capture mode, create a rectangle or ellipse area marker for the + extraction zone and set `ExtZone` to that marker name. +7. Set `LimitSuccess` to the number of HVTs that must be captured or + eliminated. +8. Set `LimitFail` if the mission should fail after too many HVT deaths in + capture mode. +9. Set reward funds, rating gain/loss, end-state behavior, and optional + `TimeLimit`. +10. Set `Prerequisite Task IDs` only if this HVT task should unlock after other + tasks succeed. +11. Sync the HVT module directly to the HVT unit or units. + +Validation: + +- Capture mode requires `ExtZone`; elimination mode does not. +- `ExtZone` must be an area marker. +- The HVT timer waits until the assigned group leader acknowledges the task. + +### Defend Task + +Use `FORGE_Module_Defend` when players must hold an area against spawned enemy +waves. + +![Defend task module placement](images/eden/defend_task_mod.jpg) + +![Defend task module parameters](images/eden/defend_task_mod_params.jpg) + +![Defense area marker placement](images/eden/defend_zone_mrkr.jpg) + +![Defense marker name](images/eden/defend_zone_mrkr_var.jpg) + +The Defend task screenshots show module placement, marker setup, enemy wave +templates, and the required sync layout. + +Setup: + +1. Create a rectangle or ellipse area marker for the defense zone. +2. Place `FORGE_Module_Defend`. +3. Set a unique `TaskID`. +4. Set `DefenseZone` to the defense marker name. +5. Set `DefendTime` to how long the area must be held. +6. Set `WaveCount`. +7. Set `WaveCooldown`. +8. Set `MinBlufor` to the minimum number of friendly players or units required + in the zone. +9. Place one or more enemy groups or units to use as wave templates. +10. Sync any unit from each enemy group to the defend module. +11. Set reward funds, rating gain/loss, and end-state behavior. +12. Set `Prerequisite Task IDs` only if this defend task should unlock after + other tasks succeed. + +Validation: + +- `DefenseZone` must be an area marker. +- Syncing one unit from an enemy group makes the whole group available as a + wave composition. +- If no enemy units are synced, the task falls back to default CSAT infantry + waves. +- The timer, waves, and empty-zone failure checks start after enough BLUFOR + enter the zone. + +## Task Module Quick Reference + +| Task Module | Sync Target | Required Marker | +| --- | --- | --- | +| `FORGE_Module_Attack` | Target units or vehicles | None | +| `FORGE_Module_Destroy` | Target objects, vehicles, or units | None | +| `FORGE_Module_Defuse` | `FORGE_Module_Explosives`, optionally `FORGE_Module_Protected` | None | +| `FORGE_Module_Delivery` | `FORGE_Module_Cargo` | `DeliveryZone` | +| `FORGE_Module_Hostage` | `FORGE_Module_Hostages` and `FORGE_Module_Shooters` | `ExtZone`, optional `CBRNZone` | +| `FORGE_Module_HVT` | HVT units | `ExtZone` when capture mode is enabled | +| `FORGE_Module_Defend` | Optional enemy units as wave templates | `DefenseZone` | + +## Mission Manager Blacklist Markers + +The dynamic mission generator avoids rectangle and ellipse area markers whose +marker name or marker text starts with `blklist`. + +Use blacklist area markers to keep generated missions out of bases, spawn +areas, training zones, or protected set pieces. + +![Blacklist marker placement](images/eden/blacklist_mrkr.jpg) + +![Blacklist marker variable name](images/eden/blacklist_mrkr_var.jpg) + +Setup: + +1. Create a rectangle or ellipse area marker over the area to exclude. +2. Set the marker variable name or marker text to start with `blklist`. +3. Give the marker real size so the generator can test candidate positions + against the area. + +## Task Setup Checklist + +Before publishing a mission, verify: + +- Every task has a unique `TaskID`. +- Every configured marker name exists in Eden. +- Zone markers are area markers, not icon-only markers. +- Grouping modules are synced in the correct direction. +- Success and fail limits match the number of required entities. +- Reward funds and rating changes are intentional. +- Unchained tasks appear in CAD when created. +- Chained tasks remain hidden until all prerequisite task IDs succeed. +- Assigned CAD tasks can be acknowledged, declined, and completed. + +## Mission Validation Checklist + +Run this checklist in a local multiplayer test: + +- Stand within 5 meters of each bank object and verify the full bank action. +- Stand within 5 meters of each ATM and verify ATM mode. +- Confirm PIN changes are only available from the full bank interface. +- Stand near each store object and complete a test checkout. +- Stand near each locker and verify the local locker/arsenal opens. +- Open each garage and retrieve/store a vehicle. +- Open each virtual garage category and confirm the correct spawn marker is + used. +- Block a garage spawn marker with a vehicle and confirm the warning appears. +- Create each mission task and confirm CAD visibility. +- Assign a task in CAD and verify the player flow through completion or failure. + +## Eden Screenshot Set + +The live docs should include real Eden screenshots for mission designers. When +capturing them, save the images under `docus/public/images/eden/` and use these +filenames so the docs can reference stable assets: + +| File | Capture | +| --- | --- | +| `bank_obj.jpg`, `bank_obj_var.jpg` | Bank object placement and variable name. | +| `atm_obj.jpg`, `atm_obj_var.jpg` | ATM object placement and variable name. | +| `store_obj.jpg`, `store_obj_var.jpg` | Store object placement and variable name. | +| `locker_obj.jpg`, `locker_obj_var.jpg` | Locker container placement and variable name. | +| `garage_obj.jpg`, `garage_obj_var.jpg` | Garage interaction object placement and variable name. | +| `garage_spawn_mrkrs.jpg`, `garage_spawn_1_mrkr_var.jpg` | Garage category spawn markers and marker variable naming. | +| `garage_obj_2.jpg`, `garage_obj_2_var.jpg`, `garage_spawn_2_mrkrs.jpg` | Additional garage site placement, variable name, and spawn markers. | +| `med_spawn_obj.jpg`, `med_spawn_obj_var.jpg` | Medical spawn object placement and variable name. | +| `ceo_unit.jpg`, `ceo_unit_var.jpg` | CEO playable unit placement and variable name. | +| `dispatch_unit.jpg`, `dispatch_unit_var.jpg` | Dispatch playable unit placement and variable name. | +| `blacklist_mrkr.jpg`, `blacklist_mrkr_var.jpg` | Mission-manager blacklist marker placement and marker variable naming. | +| `create_task_mod.jpg`, `create_task_mod_params.jpg` | Arma 3 Create Task module placement and parameters. | +| `attack_task_mod.jpg`, `attack_task_mod_params.jpg`, `attack_task_tgts.jpg` | Attack task module placement, parameters, and target sync. | +| `destroy_task_mod.jpg`, `destroy_task_mod_params.jpg`, `destroy_task_tgts.jpg` | Destroy task module placement, parameters, and target sync. | +| `defuse_task_mod.jpg`, `defuse_task_mod_params.jpg` | Defuse task module placement and parameters. | +| `defuse_explosives_mod.jpg`, `defuse_protected_mod.jpg` | Defuse grouping modules for explosive and protected entities. | +| `delivery_task_mod.jpg`, `delivery_task_mod_params.jpg`, `delivery_cargo_mod.jpg` | Delivery task module, parameters, and Cargo Entities grouping module. | +| `delivery_zone_mrkr.jpg`, `delivery_zone_mrkr_var.jpg` | Delivery area marker placement and marker name. | +| `hostage_task_mod.jpg`, `hostage_task_mod_params.jpg` | Hostage task module placement and parameters. | +| `hostage_entities_mod.jpg`, `hostage_shooters_mod.jpg` | Hostage grouping modules for hostage and shooter units. | +| `hostage_ext_zone_mrkr.jpg`, `hostage_ext_zone_mrkr_var.jpg` | Hostage extraction marker placement and marker name. | +| Hostage CBRN marker | Use the same extraction-marker capture pattern if a separate CBRN screenshot is ever needed. | +| `hvt_task_mod.jpg`, `hvt_task_mod_params.jpg` | HVT eliminate task module placement and parameters. | +| `hvt_capture_task_mod.jpg`, `hvt_capture_task_mod_params.jpg` | HVT capture task module placement and parameters. | +| `hvt_ext_zone_mrkr.jpg`, `hvt_ext_zone_mrkr_var.jpg` | HVT capture extraction marker placement and marker name. | +| `defend_task_mod.jpg`, `defend_task_mod_params.jpg` | Defend task module placement, parameters, wave templates, and sync. | +| `defend_zone_mrkr.jpg`, `defend_zone_mrkr_var.jpg` | Defense area marker placement and marker name. | +| `cad-visible-task.jpg` | In-game CAD showing a task created from the Eden module. | + +Use screenshots that show the Eden left-side entity list, the selected object's +attributes panel, and the map placement where possible. Crop only enough to +remove unrelated mission content. + +## Related Guides + +- [Task Usage Guide](./TASK_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Store Usage Guide](./STORE_USAGE_GUIDE.md) +- [Bank Usage Guide](./BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/MODULE_REFERENCE.md b/arma/forge_pmc_simulator.Tanoa/guides/MODULE_REFERENCE.md new file mode 100644 index 0000000..b673b08 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/MODULE_REFERENCE.md @@ -0,0 +1,221 @@ +# Module Reference + +This reference lists the main Forge modules and where each layer lives. + +## Directory Map + +```text +arma/client/addons/ Client-side Arma addons and browser UIs +arma/server/addons/ Server-side Arma addons and extension bridge +arma/server/extension/ Rust arma-rs extension and SurrealDB adapters +bin/icom/ Interprocess communication helper +lib/models/ Shared domain data models +lib/repositories/ Repository traits and in-memory stores +lib/services/ Domain services and workflow logic +lib/shared/ Cross-crate helpers +tools/ Web UI build tooling +docs/ Framework-level documentation +``` + +## Gameplay Domains + +| Domain | Purpose | Client addon | Server addon | Service/model layer | Extension group | +| --- | --- | --- | --- | --- | --- | +| Actor | Player identity, loadout, position, status, contact identifiers, and persistent character data. | `arma/client/addons/actor` | `arma/server/addons/actor` | `lib/models/src/actor.rs`, `lib/services/src/actor.rs` | `actor:*` | +| Bank | Player accounts, cash/bank balances, PIN validation and changes, transfers, checkout charging, and transaction context. | `arma/client/addons/bank` | `arma/server/addons/bank` | `lib/models/src/bank.rs`, `lib/services/src/bank.rs` | `bank:*`, `bank:hot:*` | +| CAD | Dispatch requests, assignments, orders, activity stream, profiles, groups, and hydrated dispatcher views. | `arma/client/addons/cad` | `arma/server/addons/cad` | `lib/models/src/cad.rs`, `lib/services/src/cad.rs` | `cad:*` | +| Garage | Player vehicle storage with plate IDs, fuel, damage, and hit point state. | `arma/client/addons/garage` | `arma/server/addons/garage` | `lib/models/src/garage.rs`, `lib/services/src/garage.rs` | `garage:*`, `garage:hot:*` | +| Locker | Player item storage keyed by classname with category and amount. | `arma/client/addons/locker` | `arma/server/addons/locker` | `lib/models/src/locker.rs`, `lib/services/src/locker.rs` | `locker:*`, `locker:hot:*` | +| Organization | Player organizations, membership, treasury, credit lines, shared assets, and fleet data. | `arma/client/addons/org` | `arma/server/addons/org` | `lib/models/src/org.rs`, `lib/services/src/org.rs` | `org:*`, `org:hot:*` | +| Phone | Contacts, messages, and email state. | `arma/client/addons/phone` | `arma/server/addons/phone` | `lib/models/src/phone.rs`, `lib/services/src/phone.rs` | `phone:*` | +| Store | Storefront entity setup, catalog hydration, checkout workflows, and checkout charging integration. | `arma/client/addons/store` | `arma/server/addons/store` | `lib/models/src/store.rs`, `lib/services/src/store.rs` | `store:checkout` | +| Task | Server-owned mission/task flows, catalog, ownership, status, participant tracking, rewards, and defuse counters. | none | `arma/server/addons/task` | `lib/models/src/task.rs`, `lib/services/src/task.rs` | `task:*` | +| Owned Garage | Organization or owner-scoped vehicle unlock storage. | via garage/org UI | server extension only | `lib/models/src/v_garage.rs`, `lib/services/src/v_garage.rs` | `owned:garage:*` | +| Owned Locker | Organization or owner-scoped arsenal unlock storage. | via locker/org UI | server extension only | `lib/models/src/v_locker.rs`, `lib/services/src/v_locker.rs` | `owned:locker:*` | + +Server and extension guides: +[Actor](./ACTOR_USAGE_GUIDE.md), +[Bank](./BANK_USAGE_GUIDE.md), +[CAD](./CAD_USAGE_GUIDE.md), +[Economy](./ECONOMY_USAGE_GUIDE.md), +[Garage](./GARAGE_USAGE_GUIDE.md), +[Locker](./LOCKER_USAGE_GUIDE.md), +[Organization](./ORG_USAGE_GUIDE.md), +[Owned Storage](./OWNED_STORAGE_USAGE_GUIDE.md), +[Phone](./PHONE_USAGE_GUIDE.md), +[Store](./STORE_USAGE_GUIDE.md), +[Task](./TASK_USAGE_GUIDE.md). + +Client guides: +[Client Overview](./CLIENT_USAGE_GUIDE.md), +[Main](./CLIENT_MAIN_USAGE_GUIDE.md), +[Common](./CLIENT_COMMON_USAGE_GUIDE.md), +[Actor](./CLIENT_ACTOR_USAGE_GUIDE.md), +[Bank](./CLIENT_BANK_USAGE_GUIDE.md), +[CAD](./CLIENT_CAD_USAGE_GUIDE.md), +[Garage](./CLIENT_GARAGE_USAGE_GUIDE.md), +[Locker](./CLIENT_LOCKER_USAGE_GUIDE.md), +[Notifications](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md), +[Organization](./CLIENT_ORG_USAGE_GUIDE.md), +[Phone](./CLIENT_PHONE_USAGE_GUIDE.md), +[Store](./CLIENT_STORE_USAGE_GUIDE.md). + +## Infrastructure Modules + +| Module | Purpose | Location | +| --- | --- | --- | +| `common` | Shared SQF helpers, base stores, utility functions, and shared UI bridge pieces. | `arma/client/addons/common`, `arma/server/addons/common` | +| `extension` | Server SQF bridge around `forge_server` extension calls and chunked transport. | `arma/server/addons/extension` | +| `main` | Mod-level configuration, pre-init wiring, and server/client startup glue. | `arma/client/addons/main`, `arma/server/addons/main` | +| `economy` | Server-side fuel, medical, and service economy helpers. Fuel and repair charge organization hot state; medical charges player bank/cash first, then organization funds with repayable member debt when personal funds cannot cover the bill. | `arma/server/addons/economy` | +| `notifications` | Client notification UI, sounds, and UI event handling. | `arma/client/addons/notifications` | +| `icom` | Rust helper for interprocess communication and event broadcasting. | `bin/icom`, `arma/server/extension/src/icom.rs` | +| `terrain` | Extension-side terrain export helper. | `arma/server/extension/src/terrain.rs` | +| `transport` | Chunked request/response handling for large extension payloads. | `arma/server/extension/src/transport.rs` | +| `surreal` | SurrealDB connection lifecycle and status reporting. | `arma/server/extension/src/surreal.rs` | + +## Extension Command Groups + +Commands are invoked with: + +```sqf +"forge_server" callExtension ["group:command", [_arg1, _arg2]]; +``` + +Nested groups use additional `:` separators, for example +`bank:hot:deposit`. + +### Core + +| Command | Purpose | +| --- | --- | +| `version` | Return the extension version string. | +| `status` | Return SurrealDB connection state. | +| `surreal:status` | Return SurrealDB connection state directly from the Surreal module. | + +### Actor + +| Command | Purpose | +| --- | --- | +| `actor:get` | Fetch actor data for a resolved player UID. | +| `actor:create` | Create actor data from JSON. | +| `actor:update` | Apply actor JSON updates. | +| `actor:exists` | Return `true` or `false`. | +| `actor:delete` | Delete actor data. | +| `actor:hot:init`, `actor:hot:get`, `actor:hot:keys`, `actor:hot:override`, `actor:hot:save`, `actor:hot:remove` | Manage actor hot state. | + +See [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) for examples. + +### Bank + +| Command | Purpose | +| --- | --- | +| `bank:get`, `bank:create`, `bank:update`, `bank:exists`, `bank:delete` | Durable bank CRUD. | +| `bank:hot:init`, `bank:hot:get`, `bank:hot:override`, `bank:hot:patch`, `bank:hot:save`, `bank:hot:remove` | Manage bank hot state. | +| `bank:hot:deposit`, `bank:hot:withdraw`, `bank:hot:deposit_earnings`, `bank:hot:transfer` | Mutate hot bank balances with operation context. | +| `bank:hot:charge_checkout` | Charge a checkout against hot bank state. | +| `bank:hot:validate_pin`, `bank:hot:change_pin` | Validate and update PINs for bank operations. | + +See [Bank Usage Guide](./BANK_USAGE_GUIDE.md) for examples. + +### Garage + +| Command | Purpose | +| --- | --- | +| `garage:create`, `garage:get`, `garage:add`, `garage:update`, `garage:patch`, `garage:remove`, `garage:delete`, `garage:exists` | Durable player garage operations. | +| `garage:hot:init`, `garage:hot:get`, `garage:hot:override`, `garage:hot:add`, `garage:hot:remove_vehicle`, `garage:hot:save`, `garage:hot:remove` | Manage player garage hot state. | + +See [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) for examples. + +### Locker + +| Command | Purpose | +| --- | --- | +| `locker:create`, `locker:get`, `locker:add`, `locker:update`, `locker:patch`, `locker:remove`, `locker:delete`, `locker:exists` | Durable player locker operations. | +| `locker:hot:init`, `locker:hot:get`, `locker:hot:override`, `locker:hot:save`, `locker:hot:remove` | Manage player locker hot state. | + +See [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) for examples. + +### Organization + +| Command | Purpose | +| --- | --- | +| `org:get`, `org:create`, `org:update`, `org:exists`, `org:delete` | Durable organization CRUD. | +| `org:assets:get`, `org:assets:update` | Manage organization assets. | +| `org:fleet:get`, `org:fleet:update` | Manage organization fleet entries. | +| `org:members:get`, `org:members:add`, `org:members:remove` | Manage organization membership. | +| `org:hot:*` | Runtime organization workflows including registration, invites, credit lines, checkout charging, assets, fleet, leave, disband, save, and remove. | + +See [Org Usage Guide](./ORG_USAGE_GUIDE.md) for examples. + +### Phone + +| Command | Purpose | +| --- | --- | +| `phone:init` | Initialize phone state for a UID. | +| `phone:contacts:list`, `phone:contacts:add`, `phone:contacts:remove` | Manage contacts. | +| `phone:messages:list`, `phone:messages:thread`, `phone:messages:send`, `phone:messages:mark_read`, `phone:messages:delete` | Manage messages. | +| `phone:emails:list`, `phone:emails:send`, `phone:emails:mark_read`, `phone:emails:delete` | Manage emails. | +| `phone:remove` | Remove phone state for a UID. | + +See [Phone Usage Guide](./PHONE_USAGE_GUIDE.md) for examples. + +### CAD + +| Command Group | Purpose | +| --- | --- | +| `cad:activity:append`, `cad:activity:recent` | Append and read recent activity. | +| `cad:assignments:list`, `cad:assignments:assign`, `cad:assignments:acknowledge`, `cad:assignments:decline`, `cad:assignments:upsert`, `cad:assignments:delete` | Manage dispatch assignments. | +| `cad:orders:list`, `cad:orders:create`, `cad:orders:create_from_context`, `cad:orders:close`, `cad:orders:upsert`, `cad:orders:delete` | Manage orders. | +| `cad:requests:list`, `cad:requests:submit`, `cad:requests:submit_from_context`, `cad:requests:close`, `cad:requests:upsert`, `cad:requests:delete` | Manage requests. | +| `cad:profiles:list`, `cad:profiles:update_from_context`, `cad:profiles:upsert`, `cad:profiles:delete` | Manage profiles. | +| `cad:groups:build` | Build grouped CAD state. | +| `cad:view:hydrate` | Build the dispatcher view model. | + +See [CAD Usage Guide](./CAD_USAGE_GUIDE.md) for examples. + +### Task + +| Command Group | Purpose | +| --- | --- | +| `task:reset` | Reset task state. | +| `task:catalog:active`, `task:catalog:get`, `task:catalog:upsert`, `task:catalog:delete` | Manage task catalog entries. | +| `task:ownership:bind`, `task:ownership:release`, `task:ownership:accept`, `task:ownership:reward_context` | Manage task ownership and rewards. | +| `task:status:set`, `task:status:get`, `task:status:clear` | Manage task status. | +| `task:defuse:increment`, `task:defuse:get` | Manage defuse counters. | +| `task:clear` | Clear task state. | + +See [Task Usage Guide](./TASK_USAGE_GUIDE.md) for examples. + +### Owned Storage + +| Command Group | Purpose | +| --- | --- | +| `owned:garage:create`, `owned:garage:fetch`, `owned:garage:get`, `owned:garage:add`, `owned:garage:remove`, `owned:garage:delete`, `owned:garage:exists` | Owner-scoped vehicle storage. | +| `owned:garage:hot:*` | Owner-scoped vehicle hot state. | +| `owned:locker:create`, `owned:locker:fetch`, `owned:locker:get`, `owned:locker:add`, `owned:locker:remove`, `owned:locker:delete`, `owned:locker:exists` | Owner-scoped item storage. | +| `owned:locker:hot:*` | Owner-scoped item hot state. | + +See [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md) for examples. + +### Other Extension Groups + +| Command Group | Purpose | +| --- | --- | +| `store:checkout` | Run store checkout behavior. | +| `icom:connect`, `icom:broadcast`, `icom:send_event` | ICOM connection and event forwarding. See [ICOM Usage Guide](./ICOM_USAGE_GUIDE.md). | +| `terrain:exportSVG` | Export terrain data as SVG. | +| `transport:invoke`, `transport:invoke_stored` | Invoke commands through transport. | +| `transport:request:append`, `transport:request:clear` | Manage stored request chunks. | +| `transport:response:get`, `transport:response:clear` | Manage stored response chunks. | + +## Rust Crates + +| Crate | Role | +| --- | --- | +| `forge-models` | Domain models and validation. Keep these serializable and free of persistence details. | +| `forge-repositories` | Repository traits and in-memory implementations. Keep these storage-agnostic. | +| `forge-services` | Business rules and workflows. Depend on repository traits, not concrete databases. | +| `forge-shared` | Cross-crate helpers. Keep dependencies light. | +| `forge-server` | Arma extension crate. Owns command registration, SurrealDB runtime wiring, and concrete storage adapters. | +| `forge-icom` | ICom helper binary and client library. | diff --git a/arma/forge_pmc_simulator.Tanoa/guides/ORG_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/ORG_USAGE_GUIDE.md new file mode 100644 index 0000000..a4571a4 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/ORG_USAGE_GUIDE.md @@ -0,0 +1,239 @@ +# Organization Usage Guide + +The organization module stores organization records, members, assets, fleet +entries, and credit lines. Durable commands manage persisted records directly. +Hot-state commands support the active organization UI workflows. + +## Storage Model + +Core organization: + +```json +{ + "id": "default", + "owner": "server", + "name": "Default Organization", + "funds": 0.0, + "reputation": 0, + "credit_lines": {} +} +``` + +Hot organization: + +```json +{ + "id": "default", + "owner": "server", + "name": "Default Organization", + "funds": 0.0, + "reputation": 0, + "credit_lines": {}, + "assets": {}, + "fleet": {}, + "members": {}, + "pending_invites": {} +} +``` + +Rules validated by the Rust service: + +- `id` must be non-empty and contain only alphanumeric characters or `_`. +- `owner` must be `server` or a 17-digit Steam UID. +- `name` cannot be empty, cannot exceed 100 characters, and cannot contain + control characters. +- `funds`, reputation, and credit line amounts cannot be negative. +- Player registration is rejected when the player already belongs to a + non-default organization. +- Player registration through the server org addon requires a $50,000 personal + funds registration fee. The fee is charged from the player's bank balance + first, then on-hand cash if needed. + +## Durable Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `org:create` | `org_id`, `org_json` | Organization JSON. | +| `org:get` | `org_id` | Organization JSON. | +| `org:update` | `org_id`, `patch_json` | Updated organization JSON. | +| `org:exists` | `org_id` | `true` or `false`. | +| `org:delete` | `org_id` | `OK`. | +| `org:assets:get` | `org_id` | Asset map JSON. | +| `org:assets:update` | `org_id`, `assets_json` | Updated asset map JSON. | +| `org:fleet:get` | `org_id` | Fleet map JSON. | +| `org:fleet:update` | `org_id`, `fleet_json` | Updated fleet map JSON. | +| `org:members:get` | `org_id` | Member array JSON. | +| `org:members:add` | `org_id`, `member_uid` | `OK`. | +| `org:members:remove` | `org_id`, `member_uid` | `OK`. | + +## Create an Organization + +The command key is authoritative for `id`. + +```sqf +private _org = createHashMapFromArray [ + ["id", _orgId], + ["owner", getPlayerUID player], + ["name", "Spearnet Logistics"], + ["funds", 0], + ["reputation", 0], + ["credit_lines", createHashMap] +]; + +private _result = "forge_server" callExtension ["org:create", [ + _orgId, + toJSON _org +]]; +``` + +## Update Organization Funds + +```sqf +private _patch = createHashMapFromArray [ + ["funds", 5000], + ["reputation", 10] +]; + +private _result = "forge_server" callExtension ["org:update", [ + _orgId, + toJSON _patch +]]; +``` + +Supported durable patch fields are `id`, `owner`, `name`, `funds`, +`reputation`, and `credit_lines`. + +## Assets and Fleet + +Assets are grouped by category, then classname. + +```sqf +private _assets = createHashMapFromArray [ + ["ammo", createHashMapFromArray [ + ["ACE_30Rnd_65x39_caseless_mag", createHashMapFromArray [ + ["classname", "ACE_30Rnd_65x39_caseless_mag"], + ["type", "ammo"], + ["quantity", 20] + ]] + ]] +]; + +"forge_server" callExtension ["org:assets:update", [_orgId, toJSON _assets]]; +``` + +Fleet is keyed by an internal fleet entry ID. + +```sqf +private _fleet = createHashMapFromArray [ + ["B_Truck_01_transport_F_0", createHashMapFromArray [ + ["classname", "B_Truck_01_transport_F"], + ["name", "Transport Truck"], + ["type", "cars"], + ["status", "Ready"], + ["damage", "0%"] + ]] +]; + +"forge_server" callExtension ["org:fleet:update", [_orgId, toJSON _fleet]]; +``` + +## Hot-State Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `org:hot:init` | `org_id` | Hot organization JSON. | +| `org:hot:get` | `org_id` | Hot organization JSON. | +| `org:hot:override` | `org_id`, `hot_org_json` | Hot organization JSON. | +| `org:hot:ensure_member` | `context_json` | Hot organization JSON. | +| `org:hot:member_invites` | `member_uid` | Invite array JSON. | +| `org:hot:register` | `context_json` | Register result JSON. | +| `org:hot:invite_member` | `context_json` | Invite result JSON. | +| `org:hot:accept_invite` | `context_json` | Invite decision result JSON. | +| `org:hot:decline_invite` | `context_json` | Invite decision result JSON. | +| `org:hot:assign_credit_line` | `context_json` | Mutation result JSON. | +| `org:hot:repay_credit_line` | `context_json` | Repayment result JSON. | +| `org:hot:charge_checkout` | `context_json` | Mutation result JSON. | +| `org:hot:add_assets` | `context_json`, `assets_json` | Mutation result JSON. | +| `org:hot:add_fleet` | `context_json`, `fleet_json` | Mutation result JSON. | +| `org:hot:leave` | `context_json` | Leave result JSON. | +| `org:hot:disband` | `context_json` | Disband result JSON. | +| `org:hot:save` | `org_id` | Current hot organization JSON and async durable save. | +| `org:hot:remove` | `org_id` | `OK`. | + +## Register from UI Context + +The server-side `forge_server_org` registration flow charges the $50,000 +registration fee before completing organization creation. If the organization +service rejects the registration, the bank charge is refunded. + +```sqf +private _context = createHashMapFromArray [ + ["requesterUid", getPlayerUID player], + ["requesterName", name player], + ["orgId", _orgId], + ["orgName", "Spearnet Logistics"], + ["existingOrgId", "default"] +]; + +private _result = "forge_server" callExtension ["org:hot:register", [toJSON _context]]; +``` + +## Invite and Accept + +```sqf +private _invite = createHashMapFromArray [ + ["requesterUid", getPlayerUID player], + ["requesterName", name player], + ["orgId", _orgId], + ["requesterIsDefaultOrgCeo", false], + ["targetUid", _targetUid], + ["targetName", _targetName], + ["targetOrgId", "default"] +]; + +"forge_server" callExtension ["org:hot:invite_member", [toJSON _invite]]; + +private _decision = createHashMapFromArray [ + ["requesterUid", _targetUid], + ["requesterName", _targetName], + ["orgId", _orgId], + ["existingOrgId", "default"] +]; + +"forge_server" callExtension ["org:hot:accept_invite", [toJSON _decision]]; +``` + +## Credit Line Checkout + +```sqf +private _credit = createHashMapFromArray [ + ["requesterUid", getPlayerUID player], + ["orgId", _orgId], + ["requesterIsDefaultOrgCeo", false], + ["memberUid", _memberUid], + ["memberName", _memberName], + ["amount", 1000] +]; + +"forge_server" callExtension ["org:hot:assign_credit_line", [toJSON _credit]]; + +private _charge = createHashMapFromArray [ + ["requesterUid", _memberUid], + ["orgId", _orgId], + ["requesterIsDefaultOrgCeo", false], + ["source", "credit_line"], + ["amount", 250], + ["commit", true] +]; + +"forge_server" callExtension ["org:hot:charge_checkout", [toJSON _charge]]; +``` + +## Error Handling + +```sqf +private _payload = _result select 0; +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Organization error: %1", _payload]; +}; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/OWNED_STORAGE_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/OWNED_STORAGE_USAGE_GUIDE.md new file mode 100644 index 0000000..1cc5223 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/OWNED_STORAGE_USAGE_GUIDE.md @@ -0,0 +1,158 @@ +# Owned Storage Usage Guide + +Owned storage covers the `owned:locker` and `owned:garage` extension command +groups. These modules store unlock lists rather than physical item or vehicle +instances. + +Use these modules for virtual arsenal and virtual garage unlocks. Use +[Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) and +[Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) for physical inventory and stored +vehicle instances. + +## Owned Locker Model + +```json +{ + "items": ["FirstAidKit"], + "weapons": ["arifle_MX_F"], + "magazines": ["30Rnd_65x39_caseless_black_mag"], + "backpacks": ["B_AssaultPack_rgr"] +} +``` + +Supported owned locker categories: + +- `items` +- `weapons` +- `magazines` +- `backpacks` + +New owned lockers are created with default unlocks from the Rust model. + +## Owned Garage Model + +```json +{ + "cars": ["B_Quadbike_01_F"], + "armor": [], + "helis": [], + "planes": [], + "naval": [], + "other": [] +} +``` + +Supported owned garage categories: + +- `cars` +- `armor` +- `helis` +- `planes` +- `naval` +- `other` + +The durable `owned:garage:remove` command currently accepts `heli` for the +helicopter category. Add, get, and hot remove accept `helis`. + +New owned garages are created with default unlocks from the Rust model. + +## Owned Locker Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `owned:locker:create` | `uid` | Full owned locker JSON. | +| `owned:locker:fetch` | `uid` | Full owned locker JSON. | +| `owned:locker:get` | `uid`, `category` | Category classname array JSON. | +| `owned:locker:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. | +| `owned:locker:remove` | `uid`, `category`, `classname` | Updated category array JSON. | +| `owned:locker:delete` | `uid` | `OK`. | +| `owned:locker:exists` | `uid` | `true` or `false`. | + +## Owned Garage Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `owned:garage:create` | `uid` | Full owned garage JSON. | +| `owned:garage:fetch` | `uid` | Full owned garage JSON. | +| `owned:garage:get` | `uid`, `category` | Category classname array JSON. | +| `owned:garage:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. | +| `owned:garage:remove` | `uid`, `category`, `classname` | Updated category array JSON. | +| `owned:garage:delete` | `uid` | `OK`. | +| `owned:garage:exists` | `uid` | `true` or `false`. | + +## Add Virtual Arsenal Unlocks + +```sqf +private _classes = ["arifle_MX_F", "hgun_P07_F"]; + +private _result = "forge_server" callExtension ["owned:locker:add", [ + getPlayerUID player, + "weapons", + toJSON _classes +]]; +``` + +## Add Virtual Garage Unlocks + +```sqf +private _classes = ["B_Quadbike_01_F", "B_MRAP_01_F"]; + +private _result = "forge_server" callExtension ["owned:garage:add", [ + getPlayerUID player, + "cars", + toJSON _classes +]]; +``` + +## Remove an Unlock + +```sqf +"forge_server" callExtension ["owned:locker:remove", [ + getPlayerUID player, + "weapons", + "arifle_MX_F" +]]; + +"forge_server" callExtension ["owned:garage:remove", [ + getPlayerUID player, + "cars", + "B_Quadbike_01_F" +]]; +``` + +## Hot-State Commands + +Both owned storage modules support hot state. + +Owned locker: + +| Command | Arguments | Returns | +| --- | --- | --- | +| `owned:locker:hot:init` | `uid` | Full owned locker JSON. | +| `owned:locker:hot:fetch` | `uid` | Full owned locker JSON. | +| `owned:locker:hot:get` | `uid`, `category` | Category array JSON. | +| `owned:locker:hot:override` | `uid`, `locker_json` | Full owned locker JSON. | +| `owned:locker:hot:save` | `uid` | Current hot owned locker JSON and async durable save. | +| `owned:locker:hot:remove` | `uid` | `OK`. | + +Owned garage: + +| Command | Arguments | Returns | +| --- | --- | --- | +| `owned:garage:hot:init` | `uid` | Full owned garage JSON. | +| `owned:garage:hot:fetch` | `uid` | Full owned garage JSON. | +| `owned:garage:hot:get` | `uid`, `category` | Category array JSON. | +| `owned:garage:hot:override` | `uid`, `garage_json` | Full owned garage JSON. | +| `owned:garage:hot:add` | `uid`, `category`, `classnames_json` | Updated category array JSON. | +| `owned:garage:hot:remove_item` | `uid`, `category`, `classname` | Updated category array JSON. | +| `owned:garage:hot:save` | `uid` | Current hot owned garage JSON and async durable save. | +| `owned:garage:hot:remove` | `uid` | `OK`. | + +## Error Handling + +```sqf +private _payload = _result select 0; +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Owned storage error: %1", _payload]; +}; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/PHONE_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/PHONE_USAGE_GUIDE.md new file mode 100644 index 0000000..6cd0829 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/PHONE_USAGE_GUIDE.md @@ -0,0 +1,136 @@ +# Phone Usage Guide + +The phone module stores contacts, messages, and emails for each UID. It is a +server-extension state module backed by SurrealDB. + +## Storage Model + +```json +{ + "contacts": ["76561198000000000", "field_commander"], + "messages": [ + { + "id": "phone-message:sender:receiver:1", + "from": "sender", + "to": "receiver", + "message": "Text body", + "timestamp": 123.45, + "read": false + } + ], + "emails": [ + { + "id": "phone-email:sender:receiver:2", + "from": "sender", + "to": "receiver", + "subject": "Subject", + "body": "Email body", + "timestamp": 123.45, + "read": false + } + ] +} +``` + +Rules validated by the Rust service: + +- UID arguments cannot be empty. +- Message and email bodies cannot be empty. +- Empty email subjects become `No subject`. +- Player messages and emails cannot target `field_commander`. +- `field_commander` can send messages or emails to players. +- Deleting a message or email removes it only from the requesting UID's index. + +## Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `phone:init` | `uid` | Full phone payload. | +| `phone:contacts:list` | `uid` | Contact UID array. | +| `phone:contacts:add` | `uid`, `contact_uid` | `true` or `false`. | +| `phone:contacts:remove` | `uid`, `contact_uid` | `true` or `false`. | +| `phone:messages:list` | `uid` | Message array. | +| `phone:messages:thread` | `uid`, `other_uid` | Message array for both participants. | +| `phone:messages:send` | `from_uid`, `to_uid`, `message`, `timestamp` | Message JSON. | +| `phone:messages:mark_read` | `uid`, `message_id` | `true` or `false`. | +| `phone:messages:delete` | `uid`, `message_id` | `true` or `false`. | +| `phone:emails:list` | `uid` | Email array. | +| `phone:emails:send` | `from_uid`, `to_uid`, `subject`, `body`, `timestamp` | Email JSON. | +| `phone:emails:mark_read` | `uid`, `email_id` | `true` or `false`. | +| `phone:emails:delete` | `uid`, `email_id` | `true` or `false`. | +| `phone:remove` | `uid` | `OK`. | + +## Initialize Phone State + +`phone:init` creates phone state if needed and seeds self-contact plus +`field_commander`. + +```sqf +private _result = "forge_server" callExtension ["phone:init", [getPlayerUID player]]; +private _payload = _result select 0; + +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Phone init failed: %1", _payload]; +}; + +private _phone = fromJSON _payload; +``` + +## Send a Message + +```sqf +private _timestamp = str diag_tickTime; + +private _result = "forge_server" callExtension ["phone:messages:send", [ + getPlayerUID player, + _targetUid, + "Move to checkpoint Alpha.", + _timestamp +]]; +``` + +## Read a Conversation + +```sqf +private _result = "forge_server" callExtension ["phone:messages:thread", [ + getPlayerUID player, + _otherUid +]]; + +private _messages = fromJSON (_result select 0); +``` + +## Send an Email + +```sqf +private _result = "forge_server" callExtension ["phone:emails:send", [ + getPlayerUID player, + _targetUid, + "Supply Request", + "Requesting resupply at grid 123456.", + str diag_tickTime +]]; +``` + +## Mark and Delete Records + +```sqf +"forge_server" callExtension ["phone:messages:mark_read", [ + getPlayerUID player, + _messageId +]]; + +"forge_server" callExtension ["phone:emails:delete", [ + getPlayerUID player, + _emailId +]]; +``` + +## Error Handling + +```sqf +private _payload = (_result select 0); +if (_payload find "Error:" == 0) then { + systemChat format ["Phone error: %1", _payload]; +}; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/PLAYER_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/PLAYER_GUIDE.md new file mode 100644 index 0000000..9016ab4 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/PLAYER_GUIDE.md @@ -0,0 +1,318 @@ +# Player Guide + +Use this guide as the player-facing overview for Forge systems. It explains +what players interact with during normal missions, how task assignment works, +and what persistent storage limits apply. + +## Opening Forge Interactions + +Most Forge actions are opened from the actor interaction menu while standing +near a configured mission object. + +![Custom interaction menu](images/player/interaction_menu.jpg) + +Press `Tab` by default to open the custom interaction menu. Server settings or +local keybind changes may use a different key. + +Known current behavior: after closing the custom interaction menu, players may +need to press `Tab` twice before it opens again. Treat this as a temporary +workaround until the interaction menu focus behavior is investigated further. + +Players usually need to be within 5 meters of an interaction object such as a +bank terminal, ATM, store counter, garage terminal, or locker. + +## CAD and Tasks + +CAD is the main task and dispatch system. It is used for mission contracts, +group status, support requests, dispatch orders, and task assignment. + +![CAD operations task board](images/player/cad_ops_board.jpg) + +Player workflow: + +1. Open CAD from the available interaction path. +2. Review available or assigned tasks. +3. If a dispatcher assigns a task to your group, the group leader must + acknowledge or decline it. +4. Once acknowledged, the task becomes active for the assigned group. +5. Complete the task objective shown by CAD, map task state, and mission + instructions. + +Map focus behavior: + +- Click an assigned or accepted task in the operations task board to center the + map on that task. +- Click a roster member to center the map on that player. +- Click a support request to center the map on the request location. +- Dispatch map mode supports the same focus behavior for groups, contracts, + and support requests. + +Dispatch workflow: + +![CAD dispatch board](images/player/cad_dispatch_board.jpg) + +1. Open CAD with a dispatcher-enabled slot or permission. +2. Use dispatch mode to review groups, open contracts, assigned contracts, and + support requests. +3. Assign available contracts to active groups. +4. Send dispatch orders or close completed orders as needed. +5. Track group status and recent CAD activity. + +Dispatch access: + +- The CEO slot can administer the default organization and use CAD dispatch + permissions. +- The Dispatch slot grants CAD dispatch permissions without default + organization administration rights. +- Players who are the CEO or owner of their own organization also receive CAD + dispatch permissions. + +Important task behavior: + +- CAD assignment reserves a task for a group. +- The task starts after the assigned group leader acknowledges it. +- If the leader declines, the task returns to the open contract board. +- Some task timers wait for group-leader acknowledgment before counting down. + +## Phone + +The phone provides contacts, messages, email, and local utility apps. + +![Phone home screen](images/player/phone_home.jpg) + +### Contacts + +Use Contacts to keep track of other players by phone number or email address. +Adding contacts makes it easier to start messages and emails without manually +entering recipient details every time. + +![Phone contacts screen](images/player/phone_contacts.jpg) + +### Messages + +Messages are short player-to-player conversations. + +![Phone messages screen](images/player/phone_messages.jpg) + +Use Messages to: + +- start or continue a conversation with a contact +- read incoming messages +- mark messages as read +- delete messages you no longer need + +### Email + +Email is used for longer player-to-player communication. + +![Phone email screen](images/player/phone_email.jpg) + +Use Email to: + +- send a subject and body to another player +- read incoming mail +- mark email as read +- delete old email + +### Local Phone Apps + +Notes, calendar events, clocks, alarms, and theme preferences are local utility +features. They are saved for the local player profile and should not be treated +as shared multiplayer data. + +## Bank and ATM + +Bank and ATM access are separate. + +Use a bank object for full banking: + +![Bank app](images/player/bank_app.jpg) + +- view account information +- transfer funds +- deposit earnings +- change PIN + +Use an ATM for limited account access: + +![ATM PIN screen](images/player/atm_app_pin.jpg) + +![ATM home screen](images/player/atm_app_home.jpg) + +- PIN-gated account actions +- ATM banking workflows +- no PIN changes + +If a PIN prompt appears, enter the correct PIN before attempting account +actions. + +## Organizations + +Players start in the default organization. A player can create a player-owned +organization only if they have `$50,000` available for the registration fee. +Organization access depends on the player's role. + +![Organization home screen](images/player/org_home.jpg) + +![Organization registration screen](images/player/org_registration.jpg) + +Default organization: + +- The `ceo` slot can administer the default organization. +- The `dispatch` slot receives CAD dispatch permissions, but does not receive + default organization administration rights. + +Player-owned organizations: + +![Organization dashboard](images/player/org_dashboard.jpg) + +![Organization treasury screen](images/player/org_treasury.jpg) + +- The player who created the organization is its owner or CEO. +- The owner can administer the organization, including treasury and roster + actions exposed by the organization interface. +- Organization owners can invite players, manage members, assign credit lines, + transfer funds or run payroll when funds are available, and disband the + organization. +- Organization owners can use organization funds for supported store purchases. +- Members may receive assigned credit lines, accept or decline organization + invites, and leave the organization. +- The organization CEO or owner cannot leave their own organization directly. + They must disband the organization if they want to leave it. + +Organization actions are server-authoritative. If an organization action fails, +check that the player has the correct role, the player or organization has +enough funds, and the target player is eligible for the action. + +## Store + +Stores sell unlocks and equipment through the configured server-side catalog. + +![Store catalog](images/player/store_catalog.jpg) + +Store purchases may grant: + +- items or equipment added to the locker +- matching gear unlocks in the virtual arsenal +- vehicle unlocks in the virtual garage +- other mission-configured rewards + +Store purchases are server-authoritative. If a purchase succeeds, the relevant +bank, locker, virtual arsenal, virtual garage, or organization state updates +from the server. + +![Store checkout result](images/player/store_checkout.jpg) + +Vehicle purchases unlock the vehicle in the virtual garage. They do not place a +physical vehicle into the player's 5-slot garage. Use the virtual garage to +spawn an unlocked vehicle, and use the garage to store or retrieve live world +vehicles. + +## Locker and Virtual Arsenal + +The locker is personal item storage. + +![Locker storage](images/player/locker.jpg) + +Locker rules: + +- Up to 25 items can be stored. +- The locker saves when the locker container is closed. +- Over-capacity storage can warn or fail depending on server handling. + +The virtual arsenal is locked down. Players only see gear they have been +granted or have unlocked through systems such as the store. The virtual arsenal +is not intended to expose the full unrestricted Arma arsenal. + +![Virtual arsenal unlocks](images/player/virtual_arsenal.jpg) + +## Garage and Virtual Garage + +The garage stores physical player vehicles that have been saved from the world. + +![Garage dashboard](images/player/garage.jpg) + +Garage rules: + +- Up to 5 vehicles can be stored. +- Stored vehicles can be retrieved from a garage interaction point. +- Retrieved vehicles become live world vehicles again. +- Vehicle service actions operate on live nearby vehicles, not vehicles that + are still stored. + +The virtual garage is locked down. Players only see vehicles they have been +granted or have unlocked through systems such as the store. Virtual garage +unlocks are separate from the 5 physical vehicle slots in the garage. The +virtual garage uses mission-configured spawn lanes, and spawning may be blocked +if the spawn position is occupied. + +![Virtual garage unlocks](images/player/virtual_garage.jpg) + +## Economy Services + +Economy services are server-controlled. Charges must succeed before the world +effect is applied. + +![Garage service controls](images/player/garage.jpg) + +### Medical + +Medical services are player-funded first. + +![Medical respawn screen](images/player/medical_respawn.jpg) + +Billing order: + +1. Player bank balance. +2. Player cash. +3. Organization funds, when allowed by the server. +4. Organization credit-line debt for the player when organization fallback is + used. + +Medical respawn placement uses mission-configured medical spawn objects. + +### Refuel + +Refuel service is organization-funded. If the organization cannot cover the +cost, the vehicle is not refueled or the fuel level is rolled back. + +Refuel is available from the garage app dashboard shown above. + +### Repair + +Repair service is organization-funded. The repair is only applied after the +organization charge succeeds. + +Repair is available from the garage app dashboard shown above. + +### Rearm + +If the mission exposes rearm service through the economy or support workflow, +expect it to follow the same server-authoritative pattern: the service request +must be accepted and billed before equipment or vehicle state changes are +applied. + +Rearm is available from the garage app dashboard shown above. + +## Common Player Checks + +If a system does not appear or does not work: + +- Move closer to the interaction object. +- Confirm you are using the correct object type, such as ATM vs bank. +- Confirm your group leader has acknowledged an assigned CAD task. +- Confirm the needed store unlock has been purchased before checking VA or VG. +- Confirm the garage spawn point is clear before using the virtual garage. +- Confirm your player, cash, bank, or organization funds can cover the service. + +## Related Guides + +- [Mission Designer Guide](./MISSION_DESIGNER_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Organization Usage Guide](./ORG_USAGE_GUIDE.md) +- [Store Usage Guide](./STORE_USAGE_GUIDE.md) +- [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/README.md b/arma/forge_pmc_simulator.Tanoa/guides/README.md new file mode 100644 index 0000000..37f5281 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/README.md @@ -0,0 +1,77 @@ +# Forge Documentation + +Forge is split into Arma client addons, Arma server addons, a Rust server +extension, shared Rust domain crates, and web UI build tooling. This directory +collects framework-level documentation for those pieces. + +## Launch Prerequisites + +Before starting a Forge-enabled dedicated server or local multiplayer test, +server owners and developers must: + +1. Start SurrealDB. +2. Place `config.toml` beside `forge_server_x64.dll`. +3. Keep the `config.toml` SurrealDB endpoint, namespace, database, username, + and password aligned with the running database. + +Mission designers and players do not need to run SurrealDB unless they are +hosting locally, but the server they join must have these prerequisites ready. +See [SurrealDB Setup](./surrealdb-setup.md) for the full setup path. + +## Start Here + +- [Framework Architecture](./FRAMEWORK_ARCHITECTURE.md): how SQF, web UIs, + Rust services, repositories, and SurrealDB fit together. +- [Module Reference](./MODULE_REFERENCE.md): module inventory for gameplay + domains, extension command groups, client addons, server addons, and Rust + crates. +- [Development Guide](./DEVELOPMENT_GUIDE.md): how to add or change a module + without breaking the framework boundaries. +- [Mission Designer Guide](./MISSION_DESIGNER_GUIDE.md): how to place Eden + objects, garage markers, and CAD-compatible task modules for playable + missions. +- [Player Guide](./PLAYER_GUIDE.md): how players use CAD, phone, bank, store, + locker, garage, and economy services during missions. +- [SurrealDB Setup](./surrealdb-setup.md): where to get SurrealDB or + Surrealist and how to connect Forge to it for local or live use. + +## Server and Extension Usage Guides + +- [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) +- [Bank Usage Guide](./BANK_USAGE_GUIDE.md) +- [CAD Usage Guide](./CAD_USAGE_GUIDE.md) +- [Economy Usage Guide](./ECONOMY_USAGE_GUIDE.md) +- [Garage Usage Guide](./GARAGE_USAGE_GUIDE.md) +- [ICOM Usage Guide](./ICOM_USAGE_GUIDE.md) +- [Locker Usage Guide](./LOCKER_USAGE_GUIDE.md) +- [Organization Usage Guide](./ORG_USAGE_GUIDE.md) +- [Owned Storage Usage Guide](./OWNED_STORAGE_USAGE_GUIDE.md) +- [Phone Usage Guide](./PHONE_USAGE_GUIDE.md) +- [Store Usage Guide](./STORE_USAGE_GUIDE.md) +- [Task Usage Guide](./TASK_USAGE_GUIDE.md) + +## Client Usage Guides + +- [Client Usage Guide](./CLIENT_USAGE_GUIDE.md) +- [Client Main Usage Guide](./CLIENT_MAIN_USAGE_GUIDE.md) +- [Client Common Usage Guide](./CLIENT_COMMON_USAGE_GUIDE.md) +- [Client Actor Usage Guide](./CLIENT_ACTOR_USAGE_GUIDE.md) +- [Client Bank Usage Guide](./CLIENT_BANK_USAGE_GUIDE.md) +- [Client CAD Usage Guide](./CLIENT_CAD_USAGE_GUIDE.md) +- [Client Garage Usage Guide](./CLIENT_GARAGE_USAGE_GUIDE.md) +- [Client Locker Usage Guide](./CLIENT_LOCKER_USAGE_GUIDE.md) +- [Client Notifications Usage Guide](./CLIENT_NOTIFICATIONS_USAGE_GUIDE.md) +- [Client Organization Usage Guide](./CLIENT_ORG_USAGE_GUIDE.md) +- [Client Phone Usage Guide](./CLIENT_PHONE_USAGE_GUIDE.md) +- [Client Store Usage Guide](./CLIENT_STORE_USAGE_GUIDE.md) + +## Related Documentation + +- [Server Extension Docs](../arma/server/docs/README.md) +- [Server Extension API Reference](../arma/server/docs/api-reference.md) +- [Server Extension Usage Examples](../arma/server/docs/usage-examples.md) +- [Client Addon Docs](../arma/client/docs/README.md) +- [Shared Rust Libraries](../lib/README.md) +- [Repository Crate](../lib/repositories/README.md) +- [Service Crate](../lib/services/README.md) +- [Model Crate](../lib/models/README.md) diff --git a/arma/forge_pmc_simulator.Tanoa/guides/STORE_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/STORE_USAGE_GUIDE.md new file mode 100644 index 0000000..f947863 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/STORE_USAGE_GUIDE.md @@ -0,0 +1,151 @@ +# Store Usage Guide + +The store module processes checkout requests. It charges a payment source and +grants purchased items to the player locker, virtual arsenal locker, and +virtual garage unlocks. + +## Server SQF Module + +The server addon uses two long-lived module objects: + +- `StorefrontStore` is the storefront workflow facade. It builds hydrate + payloads, validates checkout requests, calls the Rust `store:checkout` + command, syncs UI patches, and asks related module stores to save hot state. +- `StoreCatalogService` scans configured item and vehicle categories, builds + catalog responses, resolves checkout entries, and calculates authoritative + prices. + +Editor-placed store entities are initialized by `fnc_initStore` during store +post-init. The initializer matches non-null mission namespace objects whose +variable names contain `store` and sets `isStore = true`, following the same +pattern used by garage entities. + +## Checkout Model + +`store:checkout` accepts one JSON context. + +```json +{ + "requesterUid": "76561198000000000", + "requesterName": "Player Name", + "orgId": "default", + "requesterIsDefaultOrgCeo": false, + "paymentMethod": "bank", + "items": [ + { + "classname": "arifle_MX_F", + "category": "weapon", + "priceValue": 500, + "quantity": 1 + } + ], + "vehicles": [ + { + "classname": "B_Quadbike_01_F", + "category": "cars", + "priceValue": 1500 + } + ] +} +``` + +Rules validated by the Rust service: + +- `requesterUid` is required. +- At least one item or vehicle is required. +- The checkout total must be greater than zero. +- Item categories must be `item`, `attachment`, `weapon`, `magazine`, or + `backpack`. +- Vehicle categories must be `cars`, `armor`, `helis`, `planes`, `naval`, or + `other`. +- Payment method must be `cash`, `bank`, `org_funds`, or `credit_line`. +- Player locker capacity cannot exceed 25 unique items after checkout. +- Organization funds can only be charged by the org owner or the default org + CEO flag. + +## Command + +| Command | Arguments | Returns | +| --- | --- | --- | +| `store:checkout` | `checkout_json` | Checkout result JSON. | + +## Result Model + +```json +{ + "chargedTotal": 2000.0, + "paymentMethod": "bank", + "message": "Checkout completed. $2,000 charged, 1 locker grant(s), 1 vehicle unlock(s).", + "lockerGranted": [], + "vehicleGranted": [], + "lockerPatch": {}, + "vaPatch": {}, + "vgaragePatch": {}, + "bankPatch": {}, + "orgPatch": {}, + "orgTargetUids": [] +} +``` + +Patch fields are intended for UI updates after checkout. The service commits +all grants and payment changes together, and attempts rollback if a later write +fails. + +## Player Bank Checkout + +```sqf +private _item = createHashMapFromArray [ + ["classname", "arifle_MX_F"], + ["category", "weapon"], + ["priceValue", 500], + ["quantity", 1] +]; + +private _checkout = createHashMapFromArray [ + ["requesterUid", getPlayerUID player], + ["requesterName", name player], + ["orgId", "default"], + ["requesterIsDefaultOrgCeo", false], + ["paymentMethod", "bank"], + ["items", [_item]], + ["vehicles", []] +]; + +private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]]; +``` + +## Organization Funds Checkout + +When `paymentMethod` is `org_funds`, vehicles are also added to the +organization fleet patch. + +```sqf +private _vehicle = createHashMapFromArray [ + ["classname", "B_Quadbike_01_F"], + ["category", "cars"], + ["priceValue", 1500] +]; + +private _checkout = createHashMapFromArray [ + ["requesterUid", getPlayerUID player], + ["requesterName", name player], + ["orgId", _orgId], + ["requesterIsDefaultOrgCeo", false], + ["paymentMethod", "org_funds"], + ["items", []], + ["vehicles", [_vehicle]] +]; + +private _result = "forge_server" callExtension ["store:checkout", [toJSON _checkout]]; +``` + +## Error Handling + +```sqf +private _payload = _result select 0; +if (_payload find "Error:" == 0) exitWith { + hint format ["Checkout failed: %1", _payload]; +}; + +private _checkoutResult = fromJSON _payload; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/SURREALDB_SETUP.md b/arma/forge_pmc_simulator.Tanoa/guides/SURREALDB_SETUP.md new file mode 100644 index 0000000..b26119c --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/SURREALDB_SETUP.md @@ -0,0 +1,140 @@ +# SurrealDB Setup + +Forge uses SurrealDB for durable storage. The Rust server extension connects to +SurrealDB on startup and applies Forge schema modules automatically, so setup +comes down to running a reachable database and matching the Forge config. + +## Launch Requirement + +Before launching an Arma server or local multiplayer test with Forge enabled: + +1. Start SurrealDB and confirm it is listening on the endpoint Forge will use. +2. Copy `arma/server/extension/config.example.toml` to `config.toml` beside + `forge_server_x64.dll`. +3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace, + database, username, and password. + +Server owners and developers must do this before starting the dedicated server +or hosting a test session. Mission designers and players do not need their own +SurrealDB instance unless they are running the server locally, but the server +they connect to must have SurrealDB running and configured. + +If SurrealDB is not running, or if `config.toml` points at the wrong endpoint +or credentials, persistence-backed systems such as actors, bank accounts, +garages, lockers, organizations, phone data, stores, and tasks will not be +ready for normal gameplay. + +## Choose the Right Path + +### Developer or Server Operator + +Use this path if you are building Forge, running a local test server, or +hosting the live Arma server. + +Official SurrealDB resources: + +- [SurrealDB install page](https://surrealdb.com/install) +- [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start) + +Forge also includes helper scripts under `arma/server/surrealdb`: + +```powershell +cd arma/server/surrealdb +.\UpdateMe.bat +.\RunMe.bat +``` + +On Linux or macOS: + +```bash +cd arma/server/surrealdb +./setup.sh +./run.sh +``` + +Install SurrealDB with the official method for your platform: + +```powershell +# Windows +iwr https://windows.surrealdb.com -useb | iex +``` + +```bash +# macOS +brew install surrealdb/tap/surreal +``` + +```bash +# Linux +curl -sSf https://install.surrealdb.com | sh +``` + +For Forge, start a persistent local database instead of the default in-memory +mode: + +```powershell +surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db +``` + +`root`/`root` is only the local development default. For a public or shared +server, set a real password and keep `config.toml` aligned. + +Then copy `arma/server/extension/config.example.toml` to `config.toml` next to +`forge_server_x64.dll` and keep the values aligned with the database you +started: + +```toml +[surreal] +endpoint = "127.0.0.1:8000" +namespace = "forge" +database = "main" +username = "root" +password = "root" +connect_timeout_ms = 5000 +``` + +Before starting the game server, confirm SurrealDB is still running. After +launching the Arma server: + +1. Let the extension connect and apply the Forge schema modules. +2. Verify the connection state: + +```sqf +"forge_server" callExtension ["status", []]; +"forge_server" callExtension ["surreal:status", []]; +``` + +If you change the endpoint, namespace, database, username, or password in +SurrealDB, change the same values in Forge's `config.toml`. + +### Mission Designer or Community Manager/Leader + +Use this path if you mostly need to inspect, query, or adjust data for a test +or live server and you are not changing Forge source code. + +Official SurrealDB resources: + +- [Surrealist installation](https://surrealdb.com/docs/surrealist/installation) +- [Surrealist web app](https://app.surrealdb.com) +- [Surrealist local database serving](https://surrealdb.com/docs/surrealist/concepts/local-database-serving) + +Recommended approach: + +1. Install **Surrealist Desktop**. It is the better fit for Forge because the + official docs note that the web app can be limited when connecting to + `localhost` or non-HTTPS endpoints. +2. Connect Surrealist to the same database Forge uses. +3. Use the values from the server's `config.toml`: + +```text +Endpoint: http://127.0.0.1:8000 +Namespace: forge +Database: main +Username: root +Password: root +``` + +If you need your own local sandbox instead of connecting to an existing Forge +server, install SurrealDB first and follow the developer/server-operator path +above. Surrealist Desktop can also launch a local database for you after the +`surreal` executable is installed and available on your `PATH`. diff --git a/arma/forge_pmc_simulator.Tanoa/guides/TASK_USAGE_GUIDE.md b/arma/forge_pmc_simulator.Tanoa/guides/TASK_USAGE_GUIDE.md new file mode 100644 index 0000000..0f04a81 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/TASK_USAGE_GUIDE.md @@ -0,0 +1,585 @@ +# Task Usage Guide + +The task module stores transient mission task metadata for active server or +mission lifecycle workflows. SQF still owns Arma-only runtime state such as +objects and participants. + +The server addon at `arma/server/addons/task` also owns task execution: +creating BIS tasks, registering task entities, tracking participants, binding +task ownership, applying player/org rewards, and clearing task state when a +task completes. + +Runtime dependencies: + +- `forge_server_extension` +- `forge_server_common` +- `forge_server_actor` +- `forge_server_bank` +- `forge_server_org` +- `forge_client_notifications` + +## Data Model + +Catalog entries are flexible JSON objects. The service normalizes these fields +when a catalog entry is inserted or ownership changes: + +- `taskId` +- `taskID` +- `accepted` +- `requesterUid` +- `orgID` +- `prerequisiteTaskIds` + +Ownership context: + +```json +{ + "requesterUid": "76561198000000000", + "orgId": "default" +} +``` + +## Commands + +| Command | Arguments | Returns | +| --- | --- | --- | +| `task:reset` | none | `true`. | +| `task:catalog:active` | none | Active catalog entry array JSON. | +| `task:catalog:get` | `task_id` | Catalog entry JSON or `null`. | +| `task:catalog:upsert` | `task_id`, `entry_json` | Stored catalog entry JSON. | +| `task:catalog:delete` | `task_id` | `true`. | +| `task:ownership:bind` | `task_id`, `ownership_json` | Ownership mutation result JSON. | +| `task:ownership:release` | `task_id` | Ownership mutation result JSON. | +| `task:ownership:accept` | `task_id`, `ownership_json` | Ownership mutation result JSON. | +| `task:ownership:reward_context` | `task_id` | Reward context JSON. | +| `task:status:set` | `task_id`, `status` | `true`. | +| `task:status:get` | `task_id` | Status string JSON. | +| `task:status:clear` | `task_id` | `true`. | +| `task:defuse:increment` | `task_id` | New counter value JSON. | +| `task:defuse:get` | `task_id` | Counter value JSON. | +| `task:clear` | `task_id` | `true`. | + +## Upsert a Catalog Entry + +```sqf +private _entry = createHashMapFromArray [ + ["title", "Destroy Cache"], + ["description", "Destroy the enemy supply cache."], + ["reward", 1500] +]; + +private _result = "forge_server" callExtension ["task:catalog:upsert", [ + "task-cache-1", + toJSON _entry +]]; +``` + +## Mark a Task Active + +```sqf +"forge_server" callExtension ["task:status:set", [ + "task-cache-1", + "active" +]]; + +private _active = "forge_server" callExtension ["task:catalog:active", []]; +``` + +Completed statuses `succeeded` and `failed` are also stored as completed status +fallbacks. Clearing status removes active and completed state. + +## Accept a Task + +```sqf +private _ownership = createHashMapFromArray [ + ["requesterUid", getPlayerUID player], + ["orgId", "default"] +]; + +private _result = "forge_server" callExtension ["task:ownership:accept", [ + "task-cache-1", + toJSON _ownership +]]; +``` + +`task:ownership:accept` fails if the task is not active or another requester +already accepted it. + +## Rewards + +```sqf +private _result = "forge_server" callExtension ["task:ownership:reward_context", [ + "task-cache-1" +]]; + +private _context = fromJSON (_result select 0); +``` + +The reward context contains `requesterUid` and `orgId`. + +## Server Task Flows + +The task addon provides these server-owned task flows: + +- `attack` +- `defend` +- `defuse` +- `delivery` +- `destroy` +- `hostage` +- `hvt` + +Mission designers can create tasks in four ways: + +- Eden modules for editor-authored tasks. +- `forge_task_fnc_startTask` for script-authored tasks. +- `forge_task_fnc_handler` for pre-registered entities with reputation + gating and ownership binding. This path expects the BIS task and catalog + entry to already exist if map-task and CAD visibility are required. +- Direct task function calls for server-owned or mission-authored flows that + 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 +system-generated content rather than a hand-authored task creation path. + +## CAD Compatibility + +CAD hydrates assignable tasks from `TaskStore.getActiveTaskCatalog`. A task must +have a catalog entry and a task status of `available`, `assigned`, or `active` +before CAD can show it. +CAD assignment only reserves a task for a group. The task is accepted and task +logic starts after the assigned group leader acknowledges the assignment. If +the leader declines, the CAD assignment is removed and the task returns to the +open contract board. + +CAD-compatible creation paths: + +- Eden modules: compatible because they delegate to + `forge_task_fnc_startTask`. +- `forge_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 + uses `forge_task_fnc_startTask`. + +Limited or incompatible paths: + +- `forge_task_fnc_handler`: only compatible if a catalog entry was + already registered elsewhere. The handler sets available status and ownership, + but it does not create the BIS task shown in the map task tab or upsert the + catalog entry. +- Direct task function calls: not CAD-compatible by default. They bypass + `startTask` and usually do not register the task catalog entry or active + status that CAD hydrates from. They also only call `BIS_fnc_taskSetState` at + completion/failure; they do not create the BIS task first. + +## BIS Map Task Prerequisite + +Only the Eden task modules and `forge_task_fnc_startTask` create the BIS +task automatically through `BIS_fnc_taskCreate`. + +If a mission uses `forge_task_fnc_handler` directly or calls a task flow +function such as `forge_task_fnc_attack`, the mission must create a BIS +task with the same task ID before the Forge task completes. Otherwise the +success/failure `BIS_fnc_taskSetState` call has no visible map task to update. + +That prerequisite can be satisfied with a vanilla Eden task creation module or +a scripted `BIS_fnc_taskCreate` call. `forge_task_fnc_startTask` is the +preferred Forge path because it handles BIS task creation, Forge catalog +registration, entity registration, and handler dispatch together. + +## Eden Modules + +Eden task modules are the normal designer-facing path. Place the module, +configure its attributes, and sync it to the relevant entities or grouping +modules. + +Available task modules: + +- `FORGE_Module_Attack`: sync directly to target units or vehicles. +- `FORGE_Module_Destroy`: sync directly to objects, vehicles, or units. +- `FORGE_Module_Defuse`: sync to `FORGE_Module_Explosives` and optionally + `FORGE_Module_Protected`. +- `FORGE_Module_Delivery`: sync to `FORGE_Module_Cargo`; the cargo module syncs + to cargo objects. +- `FORGE_Module_Hostage`: sync to `FORGE_Module_Hostages` and + `FORGE_Module_Shooters`. +- `FORGE_Module_HVT`: sync directly to HVT units. +- `FORGE_Module_Defend`: configure the defense marker and wave settings; sync + enemy units to use their groups as wave templates. + +These modules delegate to `forge_task_fnc_startTask`. + +Each task module also includes an optional chain field: + +- `Prerequisite Task IDs`: comma-separated task IDs that must succeed first. + +## Mission Designer Guide + +This section is the practical Eden setup guide for mission designers. + +### General Rules + +Use these rules for every Forge task: + +1. Give every task a unique `TaskID`. +2. Use area markers for zone-style fields such as: + - `DefenseZone` + - `DeliveryZone` + - `ExtZone` + - `CBRNZone` +3. Prefer `RECTANGLE` or `ELLIPSE` markers with real size. +4. Set success and fail limits explicitly instead of relying on defaults. +5. If a task uses a timer, the countdown now waits until the assigned group + leader acknowledges the task. +6. Grouping modules such as `Explosive Entities`, `Protected Entities`, + `Cargo`, `Hostages`, and `Shooters` should be synced to real world objects, + not other logic modules. +7. To chain tasks, set `Prerequisite Task IDs` on the dependent task module. + Use comma-separated IDs such as `attack_01, delivery_02`. The dependent + task stays hidden from CAD and cannot be assigned until every listed task + succeeds. +8. Reward class fields accept comma-separated class names without brackets, + such as `ItemGPS, FirstAidKit`. Legacy SQF array strings such as + `["ItemGPS","FirstAidKit"]` are still supported. + +### Attack Task + +Use `FORGE_Module_Attack` when players need to eliminate hostile units or +vehicles. + +Setup: + +1. Place the enemy units or vehicles. +2. Place `FORGE_Module_Attack`. +3. Set `TaskID`. +4. Set `LimitSuccess` to the number of targets that must be killed. +5. Set `LimitFail` if you want a fail threshold. +6. Set rewards, rating, and optional `TimeLimit`. +7. Sync the attack module directly to the target units or vehicles. + +Notes: + +- This module reads its synced entities directly. +- `TimeLimit` uses seconds. `0` means no limit. + +### Destroy Task + +Use `FORGE_Module_Destroy` when players must destroy objects, vehicles, or +units. + +Setup: + +1. Place the objects, vehicles, or units that must be destroyed. +2. Place `FORGE_Module_Destroy`. +3. Set `TaskID`. +4. Set `LimitSuccess` to the number of targets that must be destroyed. +5. Set `LimitFail` if the mission should fail after too many losses. +6. Set rewards, rating, and optional `TimeLimit`. +7. Sync the destroy module directly to the targets. + +Notes: + +- This module reads its synced entities directly. +- `TimeLimit` uses seconds. `0` means no limit. + +### Defuse Task + +Use `FORGE_Module_Defuse` when players must defuse one or more explosives while +protecting other entities. + +Required module layout: + +```text +[Defuse Task] --> [Explosive Entities] --> explosive objects +[Defuse Task] --> [Protected Entities] --> protected objects/vehicles/units +``` + +Setup: + +1. Place the explosive objects that players must defuse. +2. Place `FORGE_Module_Explosives`. +3. Sync each explosive object to `FORGE_Module_Explosives`. +4. Place the objects, vehicles, or units that must survive. +5. Place `FORGE_Module_Protected`. +6. Sync each protected entity to `FORGE_Module_Protected`. +7. Place `FORGE_Module_Defuse`. +8. Set `TaskID`. +9. Set `LimitSuccess` to the number of explosives that must be defused. +10. Set `LimitFail` to the number of protected entities that can be lost before failure. +11. Set `TimeLimit` to the IED countdown in seconds. This is per-IED countdown behavior, not a global mission timer. +12. Set rewards, rating, and end-state options. +13. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`. +14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected`. + +Notes: + +- The module reads grouped objects from the `Explosive Entities` and + `Protected Entities` modules, not from direct object syncs. +- Logic objects are filtered out already, so only real explosives and protected + entities are counted. +- The ACE defuse event is wired to the task system and resolves IEDs back to + the correct task. + +### Delivery Task + +Use `FORGE_Module_Delivery` when players must move cargo into a delivery zone. + +Required module layout: + +```text +[Delivery Task] --> [Cargo] --> cargo objects +``` + +Setup: + +1. Place the cargo objects. +2. Create an area marker for the delivery zone. +3. Place `FORGE_Module_Cargo`. +4. Sync each cargo object to `FORGE_Module_Cargo`. +5. Place `FORGE_Module_Delivery`. +6. Set `TaskID`. +7. Set `DeliveryZone` to the marker name. +8. Set `LimitSuccess` to the number of cargo objects that must arrive. +9. Set `LimitFail` to the number of cargo objects that can be damaged past the fail threshold. +10. Set rewards, rating, and optional `TimeLimit`. +11. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`. + +Notes: + +- The runtime checks `inArea DeliveryZone`, so the zone must be an area marker. + +### Hostage Task + +Use `FORGE_Module_Hostage` when players must rescue hostages and move them to +an extraction zone. + +Required module layout: + +```text +[Hostage Task] --> [Hostage Entities] --> hostage units +[Hostage Task] --> [Shooter Entities] --> hostile shooter units +``` + +Setup: + +1. Place the hostage AI units. +2. Place the hostile shooter AI units. +3. Create an area marker for the extraction zone. +4. If using the CBRN variant, create an area marker for the `CBRNZone`. +5. Place `FORGE_Module_Hostages`. +6. Sync the hostage units to `FORGE_Module_Hostages`. +7. Place `FORGE_Module_Shooters`. +8. Sync the shooter units to `FORGE_Module_Shooters`. +9. Place `FORGE_Module_Hostage`. +10. Set `TaskID`. +11. Set `ExtZone` to the extraction marker name. +12. Set `LimitSuccess` to the number of hostages that must be rescued. +13. Set `LimitFail` to the number of hostages that can be lost before failure. +14. Set `Execution` or `CBRN` as needed for the mission variant. +15. If `CBRN` is enabled, set `CBRNZone`. +16. Set rewards, rating, and optional `TimeLimit`. +17. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`. +18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`. + +Notes: + +- Hostages and shooters are filtered to real units only. +- Hostages are protected immediately on task registration to avoid startup race conditions. +- The hostage timer now waits until the assigned group leader acknowledges the + task before counting down. +- `ExtZone` is checked with `inArea`, so it must be an area marker. + +### HVT Task + +Use `FORGE_Module_HVT` when players must capture or eliminate a high-value +target. + +Setup: + +1. Place the HVT unit or units. +2. If using capture mode, create an area marker for the extraction zone. +3. Place `FORGE_Module_HVT`. +4. Set `TaskID`. +5. Set `CaptureHVT` as needed: + - enabled for capture/extract + - disabled for kill/eliminate +6. If using capture mode, set `ExtZone` to the extraction marker name. +7. Set `LimitSuccess` to the number of HVTs that must be captured or eliminated. +8. Set `LimitFail` if the mission should fail after too many HVT deaths in capture mode. +9. Set rewards, rating, and optional `TimeLimit`. +10. Sync the HVT module directly to the HVT unit or units. + +Notes: + +- Capture mode uses `ExtZone` with `inArea`, so use an area marker. +- Elimination mode does not require an extraction zone. +- The HVT timer now waits until the assigned group leader acknowledges the task + before counting down. + +### Defend Task + +Use `FORGE_Module_Defend` when players must hold an area against spawned enemy +waves. + +Setup: + +1. Create an area marker for the defense zone. +2. Place `FORGE_Module_Defend`. +3. Set `TaskID`. +4. Set `DefenseZone` to the defense marker name. +5. Set `DefendTime` to how long the area must be held. +6. Set `WaveCount`. +7. Set `WaveCooldown`. +8. Set `MinBlufor` to the minimum number of friendlies required in the zone. +9. Place one or more enemy groups or units to use as wave templates. +10. Sync any unit from each enemy group to the defend module. +11. Set rewards, rating, and end-state options. + +Notes: + +- Synced enemy units are treated as templates. Syncing one unit from a group + makes the whole group available as a wave composition. +- If no enemy units are synced, the defend task falls back to default CSAT + infantry waves. +- The defend task waits for the required number of BLUFOR to enter the zone + before the timer, waves, and empty-zone failure checks begin. +- `DefenseZone` must be an area marker. + +### Quick Reference + +Use direct syncs: + +- `Attack Task` -> target units/vehicles +- `Destroy Task` -> target objects/vehicles/units +- `HVT Task` -> HVT units + +Use grouping modules: + +- `Defuse Task` -> `Explosive Entities`, `Protected Entities` +- `Delivery Task` -> `Cargo` +- `Hostage Task` -> `Hostage Entities`, `Shooter Entities` + +Use area markers: + +- `DefenseZone` +- `DeliveryZone` +- `ExtZone` +- `CBRNZone` + +## Scripted Start Task + +Use `forge_task_fnc_startTask` when creating tasks from modules, +mission scripts, or generated mission-manager content. It registers task +entities, creates the BIS task, stores the catalog entry, then dispatches +through `forge_task_fnc_handler`. + +```sqf +[ + "attack", + "compound_attack_01", + getPosATL leader1, + "Attack: East Compound", + "Eliminate all hostile forces.", + createHashMapFromArray [["targets", [unit1, unit2, unit3]]], + createHashMapFromArray [ + ["limitFail", 0], + ["limitSuccess", 3], + ["prerequisiteTaskIds", ["recon_01"]], + ["funds", 50000], + ["ratingFail", -10], + ["ratingSuccess", 20], + ["timeLimit", 900] + ], + 0, + getPlayerUID player, + "script" +] call forge_task_fnc_startTask; +``` + +## Chained Tasks + +Use `prerequisiteTaskIds` when a task should stay hidden until one or more +other tasks succeed. The task is still registered during mission setup, but it +is stored with `locked` status, filtered out of CAD, blocked from assignment, +and its task logic does not start until every prerequisite task has completed +with `succeeded`. + +```sqf +[ + "delivery", + "supply_delivery_02", + getMarkerPos "delivery_zone_02", + "Deliver Medical Supplies", + "Move the cargo into the marked delivery area.", + createHashMapFromArray [["cargo", [cargoBox1, cargoBox2]]], + createHashMapFromArray [ + ["deliveryZone", "delivery_zone_02"], + ["limitSuccess", 2], + ["prerequisiteTaskIds", ["compound_attack_01"]], + ["funds", 30000] + ] +] call forge_task_fnc_startTask; +``` + +Notes: + +- `prerequisiteTaskIds` accepts either a string or an array of task ID strings. +- All prerequisite tasks must succeed before the chained task unlocks. +- If a prerequisite fails or never completes, the chained task remains locked. + +## Handler Calls + +Use `forge_task_fnc_handler` directly when the task entities are already +registered and you want reputation gating plus ownership binding. Create the +BIS task and catalog entry separately if this task should appear in the map +task tab or CAD: + +```sqf +[ + "delivery", + ["delivery_1", 1, 3, "delivery_zone", 250000, -75, 300, false, false, 900], + 250, + getPlayerUID player +] call forge_task_fnc_handler; +``` + +## Direct Task Calls + +Direct task function calls still work for mission-authored or server-owned +tasks, but they do not provide a requester UID. Ownership falls back to the +`default` org. Create the BIS task separately if this task should appear in the +map task tab. + +## Timer Semantics + +Task time limits use `0` for no limit: + +- attack `timeLimit` +- destroy `timeLimit` +- delivery `timeLimit` +- hostage `timeLimit` +- HVT `timeLimit` + +Positive values are measured in seconds. Do not pass `-1` as a no-limit value; +the task runtime treats any non-zero task time limit as active. + +Defuse IED timers are different. `iedTimer` must be greater than `0`, because +IEDs are expected to have an active countdown. The Eden defuse module defaults +to `300` seconds. + +## Defuse Counter + +```sqf +"forge_server" callExtension ["task:defuse:increment", ["task-cache-1"]]; +private _count = "forge_server" callExtension ["task:defuse:get", ["task-cache-1"]]; +``` + +## Error Handling + +```sqf +private _payload = _result select 0; +if (_payload find "Error:" == 0) exitWith { + systemChat format ["Task error: %1", _payload]; +}; +``` diff --git a/arma/forge_pmc_simulator.Tanoa/guides/surrealdb-setup.md b/arma/forge_pmc_simulator.Tanoa/guides/surrealdb-setup.md new file mode 100644 index 0000000..b26119c --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/guides/surrealdb-setup.md @@ -0,0 +1,140 @@ +# SurrealDB Setup + +Forge uses SurrealDB for durable storage. The Rust server extension connects to +SurrealDB on startup and applies Forge schema modules automatically, so setup +comes down to running a reachable database and matching the Forge config. + +## Launch Requirement + +Before launching an Arma server or local multiplayer test with Forge enabled: + +1. Start SurrealDB and confirm it is listening on the endpoint Forge will use. +2. Copy `arma/server/extension/config.example.toml` to `config.toml` beside + `forge_server_x64.dll`. +3. Make sure `config.toml` matches the running SurrealDB endpoint, namespace, + database, username, and password. + +Server owners and developers must do this before starting the dedicated server +or hosting a test session. Mission designers and players do not need their own +SurrealDB instance unless they are running the server locally, but the server +they connect to must have SurrealDB running and configured. + +If SurrealDB is not running, or if `config.toml` points at the wrong endpoint +or credentials, persistence-backed systems such as actors, bank accounts, +garages, lockers, organizations, phone data, stores, and tasks will not be +ready for normal gameplay. + +## Choose the Right Path + +### Developer or Server Operator + +Use this path if you are building Forge, running a local test server, or +hosting the live Arma server. + +Official SurrealDB resources: + +- [SurrealDB install page](https://surrealdb.com/install) +- [SurrealDB CLI `start` reference](https://surrealdb.com/docs/surrealdb/cli/start) + +Forge also includes helper scripts under `arma/server/surrealdb`: + +```powershell +cd arma/server/surrealdb +.\UpdateMe.bat +.\RunMe.bat +``` + +On Linux or macOS: + +```bash +cd arma/server/surrealdb +./setup.sh +./run.sh +``` + +Install SurrealDB with the official method for your platform: + +```powershell +# Windows +iwr https://windows.surrealdb.com -useb | iex +``` + +```bash +# macOS +brew install surrealdb/tap/surreal +``` + +```bash +# Linux +curl -sSf https://install.surrealdb.com | sh +``` + +For Forge, start a persistent local database instead of the default in-memory +mode: + +```powershell +surreal start --user root --pass root --bind 127.0.0.1:8000 rocksdb://forge.db +``` + +`root`/`root` is only the local development default. For a public or shared +server, set a real password and keep `config.toml` aligned. + +Then copy `arma/server/extension/config.example.toml` to `config.toml` next to +`forge_server_x64.dll` and keep the values aligned with the database you +started: + +```toml +[surreal] +endpoint = "127.0.0.1:8000" +namespace = "forge" +database = "main" +username = "root" +password = "root" +connect_timeout_ms = 5000 +``` + +Before starting the game server, confirm SurrealDB is still running. After +launching the Arma server: + +1. Let the extension connect and apply the Forge schema modules. +2. Verify the connection state: + +```sqf +"forge_server" callExtension ["status", []]; +"forge_server" callExtension ["surreal:status", []]; +``` + +If you change the endpoint, namespace, database, username, or password in +SurrealDB, change the same values in Forge's `config.toml`. + +### Mission Designer or Community Manager/Leader + +Use this path if you mostly need to inspect, query, or adjust data for a test +or live server and you are not changing Forge source code. + +Official SurrealDB resources: + +- [Surrealist installation](https://surrealdb.com/docs/surrealist/installation) +- [Surrealist web app](https://app.surrealdb.com) +- [Surrealist local database serving](https://surrealdb.com/docs/surrealist/concepts/local-database-serving) + +Recommended approach: + +1. Install **Surrealist Desktop**. It is the better fit for Forge because the + official docs note that the web app can be limited when connecting to + `localhost` or non-HTTPS endpoints. +2. Connect Surrealist to the same database Forge uses. +3. Use the values from the server's `config.toml`: + +```text +Endpoint: http://127.0.0.1:8000 +Namespace: forge +Database: main +Username: root +Password: root +``` + +If you need your own local sandbox instead of connecting to an existing Forge +server, install SurrealDB first and follow the developer/server-operator path +above. Surrealist Desktop can also launch a local database for you after the +`surreal` executable is installed and available on your `PATH`. diff --git a/arma/forge_pmc_simulator.Tanoa/img/000.jpg b/arma/forge_pmc_simulator.Tanoa/img/000.jpg new file mode 100644 index 0000000..05b7a37 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/000.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/001.jpg b/arma/forge_pmc_simulator.Tanoa/img/001.jpg new file mode 100644 index 0000000..f88f484 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/001.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/002.jpg b/arma/forge_pmc_simulator.Tanoa/img/002.jpg new file mode 100644 index 0000000..11a00c5 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/002.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/003.jpg b/arma/forge_pmc_simulator.Tanoa/img/003.jpg new file mode 100644 index 0000000..d925a82 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/003.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/004.jpg b/arma/forge_pmc_simulator.Tanoa/img/004.jpg new file mode 100644 index 0000000..588c2e4 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/004.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/005.jpg b/arma/forge_pmc_simulator.Tanoa/img/005.jpg new file mode 100644 index 0000000..95b713c Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/005.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/006.jpg b/arma/forge_pmc_simulator.Tanoa/img/006.jpg new file mode 100644 index 0000000..c2811a3 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/006.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/007.jpg b/arma/forge_pmc_simulator.Tanoa/img/007.jpg new file mode 100644 index 0000000..b565963 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/007.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/010.jpg b/arma/forge_pmc_simulator.Tanoa/img/010.jpg new file mode 100644 index 0000000..fbf19aa Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/010.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/011.jpg b/arma/forge_pmc_simulator.Tanoa/img/011.jpg new file mode 100644 index 0000000..b88e454 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/011.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/012.jpg b/arma/forge_pmc_simulator.Tanoa/img/012.jpg new file mode 100644 index 0000000..749129a Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/012.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/013.jpg b/arma/forge_pmc_simulator.Tanoa/img/013.jpg new file mode 100644 index 0000000..a676d64 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/013.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/Datasheet_EKM4000.pdf b/arma/forge_pmc_simulator.Tanoa/img/Datasheet_EKM4000.pdf new file mode 100644 index 0000000..1a84b17 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/Datasheet_EKM4000.pdf differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/StopTutti.jpg b/arma/forge_pmc_simulator.Tanoa/img/StopTutti.jpg new file mode 100644 index 0000000..5a692e5 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/StopTutti.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/a011.jpg b/arma/forge_pmc_simulator.Tanoa/img/a011.jpg new file mode 100644 index 0000000..28ee5ba Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/a011.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/a012.jpg b/arma/forge_pmc_simulator.Tanoa/img/a012.jpg new file mode 100644 index 0000000..49052d9 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/a012.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/a013.jpg b/arma/forge_pmc_simulator.Tanoa/img/a013.jpg new file mode 100644 index 0000000..246fc74 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/a013.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/a014.jpg b/arma/forge_pmc_simulator.Tanoa/img/a014.jpg new file mode 100644 index 0000000..4fa281e Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/a014.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/armi02.jpg b/arma/forge_pmc_simulator.Tanoa/img/armi02.jpg new file mode 100644 index 0000000..673158d Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/armi02.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/armi03.jpg b/arma/forge_pmc_simulator.Tanoa/img/armi03.jpg new file mode 100644 index 0000000..80fa074 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/armi03.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/eng2.jpg b/arma/forge_pmc_simulator.Tanoa/img/eng2.jpg new file mode 100644 index 0000000..a9b3594 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/eng2.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/eng3.jpg b/arma/forge_pmc_simulator.Tanoa/img/eng3.jpg new file mode 100644 index 0000000..2b25f0d Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/eng3.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/lavagna01.jpg b/arma/forge_pmc_simulator.Tanoa/img/lavagna01.jpg new file mode 100644 index 0000000..dcc851b Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/lavagna01.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/lavagna02.jpg b/arma/forge_pmc_simulator.Tanoa/img/lavagna02.jpg new file mode 100644 index 0000000..d43f874 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/lavagna02.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/lavagna03.jpg b/arma/forge_pmc_simulator.Tanoa/img/lavagna03.jpg new file mode 100644 index 0000000..4f6bbaa Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/lavagna03.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/lavagna04.jpg b/arma/forge_pmc_simulator.Tanoa/img/lavagna04.jpg new file mode 100644 index 0000000..95b0edb Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/lavagna04.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/medical01.jpg b/arma/forge_pmc_simulator.Tanoa/img/medical01.jpg new file mode 100644 index 0000000..9d5ad48 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/medical01.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/medical02.jpg b/arma/forge_pmc_simulator.Tanoa/img/medical02.jpg new file mode 100644 index 0000000..f8b0e4c Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/medical02.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/medical03.jpg b/arma/forge_pmc_simulator.Tanoa/img/medical03.jpg new file mode 100644 index 0000000..0f1c7bd Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/medical03.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/medical04.jpg b/arma/forge_pmc_simulator.Tanoa/img/medical04.jpg new file mode 100644 index 0000000..8e7908d Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/medical04.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/medical05.jpg b/arma/forge_pmc_simulator.Tanoa/img/medical05.jpg new file mode 100644 index 0000000..cfc8ff7 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/medical05.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/radar.jpg b/arma/forge_pmc_simulator.Tanoa/img/radar.jpg new file mode 100644 index 0000000..51452d5 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/radar.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/satellite01.jpg b/arma/forge_pmc_simulator.Tanoa/img/satellite01.jpg new file mode 100644 index 0000000..f1f113c Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/satellite01.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/satellite02.jpg b/arma/forge_pmc_simulator.Tanoa/img/satellite02.jpg new file mode 100644 index 0000000..ee5081d Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/satellite02.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/uav001.jpg b/arma/forge_pmc_simulator.Tanoa/img/uav001.jpg new file mode 100644 index 0000000..6539c8b Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/uav001.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/uav002.jpg b/arma/forge_pmc_simulator.Tanoa/img/uav002.jpg new file mode 100644 index 0000000..0a69c98 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/uav002.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/uav003.jpg b/arma/forge_pmc_simulator.Tanoa/img/uav003.jpg new file mode 100644 index 0000000..774f537 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/uav003.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/uav004.jpg b/arma/forge_pmc_simulator.Tanoa/img/uav004.jpg new file mode 100644 index 0000000..50cbdb2 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/uav004.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/wanted01.jpg b/arma/forge_pmc_simulator.Tanoa/img/wanted01.jpg new file mode 100644 index 0000000..a115f7f Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/wanted01.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/img/wanted02.jpg b/arma/forge_pmc_simulator.Tanoa/img/wanted02.jpg new file mode 100644 index 0000000..72a0571 Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/img/wanted02.jpg differ diff --git a/arma/forge_pmc_simulator.Tanoa/init.sqf b/arma/forge_pmc_simulator.Tanoa/init.sqf new file mode 100644 index 0000000..e69de29 diff --git a/arma/forge_pmc_simulator.Tanoa/initPlayerLocal.sqf b/arma/forge_pmc_simulator.Tanoa/initPlayerLocal.sqf new file mode 100644 index 0000000..2c2879f --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/initPlayerLocal.sqf @@ -0,0 +1,13 @@ +[] spawn { + waitUntil { !isNull findDisplay 46 }; + sleep 1; + + private _ceoUnit = missionNamespace getVariable ["ceo", objNull]; + private _isCeoSlot = + (!isNull _ceoUnit && { player isEqualTo _ceoUnit }) || + { toLowerANSI (vehicleVarName player) isEqualTo "ceo" }; + + if (_isCeoSlot && { !(missionNamespace getVariable ["forge_pmc_missionSettingsApplied", false]) }) then { + [] call forge_pmc_fnc_openMissionSetupUI; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/initServer.sqf b/arma/forge_pmc_simulator.Tanoa/initServer.sqf new file mode 100644 index 0000000..ae00256 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/initServer.sqf @@ -0,0 +1,14 @@ +if (isServer) then { + [] spawn { + while { true } do { + { + _x addCuratorEditableObjects + [ + entities [[], ["Logic"], true /* Include vehicle crew */, true /* Exclude dead bodies */], + true + ]; + } count allCurators; + sleep 30; // Change to whatever fits your needs + }; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/mission.sqm b/arma/forge_pmc_simulator.Tanoa/mission.sqm new file mode 100644 index 0000000..c2b6bbb Binary files /dev/null and b/arma/forge_pmc_simulator.Tanoa/mission.sqm differ diff --git a/arma/forge_pmc_simulator.Tanoa/test.sqf b/arma/forge_pmc_simulator.Tanoa/test.sqf new file mode 100644 index 0000000..4885f70 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/test.sqf @@ -0,0 +1,35 @@ +suitable[] = {"attack", "defend", "hostage", "destroy"}; + +/* Create a sqf file using fnc_attackMissionGenerator.sqf as a template for the task +infomation of appropriate formating and syntax for all the code can be found in the files located in the Guides folder */ + +/* Create a dynamic mission generator for this mission using the flowing points + +- Generate a UI for setting the mission defaults that is only available to the player slots of CEO or Dispatch +- tickbox for selecting to either use the dialog or use the CfgMissions.sqf +- include options to set the following * points +* min/max money reward +* min/max reputation +* min/max penalty +* min/max timelimit +- pull all factions excluding blufor for enemy selection +- infomation of appropriate formating for the code can be found in the files located in the Guides folder */ + +createHashMapFromArray [ + ["funds", _logic getVariable ["CompanyFunds", 0]], + ["ratingFail", _logic getVariable ["RatingFail", 0]], + ["ratingSuccess", _logic getVariable ["RatingSuccess", 0]], + ["endSuccess", _logic getVariable ["EndSuccess", false]], + ["endFail", _logic getVariable ["EndFail", false]], + ["defenseZone", _defenseZone], + ["defendTime", _logic getVariable ["DefendTime", 600]], + ["waveCount", _logic getVariable ["WaveCount", 3]], + ["waveCooldown", _logic getVariable ["WaveCooldown", 300]], + ["minBlufor", _logic getVariable ["MinBlufor", 1]], + ["enemyTemplates", _templateGroups], + ["equipment", _equipmentRewards], + ["supplies", _supplyRewards], + ["weapons", _weaponRewards], + ["vehicles", _vehicleRewards], + ["special", _specialRewards] +] diff --git a/arma/forge_pmc_simulator.Tanoa/ui/MissionSetup.hpp b/arma/forge_pmc_simulator.Tanoa/ui/MissionSetup.hpp new file mode 100644 index 0000000..bc3efba --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/ui/MissionSetup.hpp @@ -0,0 +1,21 @@ +class RscPmcMissionSetup { + idd = 92010; + fadeIn = 0; + fadeOut = 0; + duration = 1e011; + onLoad = "uiNamespace setVariable ['RscPmcMissionSetup', _this select 0]"; + onUnLoad = "uiNamespace setVariable ['RscPmcMissionSetup', nil]"; + + class controlsBackground {}; + class controls { + class IFrame: RscText { + type = 106; + idc = 92011; + x = "safeZoneXAbs + (safeZoneWAbs * 0.125)"; + y = "safeZoneY + (safeZoneH * 0.125)"; + w = "safeZoneWAbs * 0.75"; + h = "safeZoneH * 0.75"; + colorBackground[] = {0, 0, 0, 0}; + }; + }; +}; diff --git a/arma/forge_pmc_simulator.Tanoa/ui/_site/index.html b/arma/forge_pmc_simulator.Tanoa/ui/_site/index.html new file mode 100644 index 0000000..d755569 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/ui/_site/index.html @@ -0,0 +1 @@ +FORGE PMC Setup
diff --git a/arma/forge_pmc_simulator.Tanoa/ui/_site/mission-setup.css b/arma/forge_pmc_simulator.Tanoa/ui/_site/mission-setup.css new file mode 100644 index 0000000..7f3e8ca --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/ui/_site/mission-setup.css @@ -0,0 +1,269 @@ +:root { + --bg-app: rgba(9, 12, 18, 0.88); + --surface: rgba(20, 24, 33, 0.9); + --surface-alt: rgba(17, 21, 30, 0.82); + --surface-deep: rgba(11, 17, 24, 0.78); + --border: rgba(255, 255, 255, 0.1); + --border-strong: rgba(255, 255, 255, 0.2); + --text-main: rgba(245, 248, 255, 0.92); + --text-muted: rgba(245, 248, 255, 0.62); + --text-subtle: rgba(245, 248, 255, 0.42); + --accent: rgba(104, 196, 255, 0.95); + --accent-soft: rgba(104, 196, 255, 0.13); + --accent-mid: rgba(91, 187, 255, 0.72); + --accent-wash: rgba(41, 69, 93, 0.18); + --danger: rgba(255, 138, 128, 0.95); + --danger-bg: rgba(92, 18, 18, 0.78); + --shadow: 0 20px 60px rgba(0, 0, 0, 0.55); +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + width: 100%; + height: 100%; + margin: 0; +} + +body { + overflow: hidden; + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; + color: var(--text-main); + background: + radial-gradient(circle at top left, var(--accent-wash), transparent 30%), + linear-gradient(180deg, rgba(9, 14, 20, 0.96), rgba(15, 22, 31, 0.98)), + var(--bg-app); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +.shell { + height: 100%; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.titlebar { + min-height: 3.25rem; + padding: 0 1.6rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); + background: linear-gradient( + 90deg, + rgba(16, 22, 31, 0.96), + rgba(19, 26, 36, 0.94) 55%, + rgba(15, 20, 28, 0.96) + ); +} + +.brand { + display: flex; + align-items: baseline; + gap: 0.8rem; +} + +.kicker { + color: var(--accent); + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.title { + font-size: 1rem; + font-weight: 700; + color: var(--text-main); +} + +option { + background: rgb(20, 24, 33); + color: rgb(245, 248, 255); +} + +.close { + width: 2rem; + height: 2rem; + border: 1px solid var(--border); + background: rgba(255, 96, 96, 0.1); + color: rgba(255, 220, 220, 0.95); + font-size: 1.15rem; +} + +.content { + min-height: 0; + padding: 1.5rem; + overflow: auto; +} + +.grid { + max-width: 78rem; + margin: 0 auto; + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: 1rem; +} + +.panel { + min-width: 0; + border: 1px solid var(--border); + background: linear-gradient(180deg, rgba(23, 31, 40, 0.86), var(--surface) 9rem); + box-shadow: var(--shadow); +} + +.panel-head { + padding: 1.15rem 1.25rem; + border-bottom: 1px solid var(--border); +} + +.panel-head h1, +.panel-head h2 { + margin: 0.2rem 0 0; + font-size: 1.45rem; + letter-spacing: 0; +} + +.panel-head p { + margin: 0.5rem 0 0; + color: var(--text-muted); +} + +.form { + padding: 1.25rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.field { + display: grid; + gap: 0.45rem; +} + +.field.wide { + grid-column: 1 / -1; +} + +label { + color: var(--text-subtle); + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +input, +select { + width: 100%; + min-height: 2.65rem; + padding: 0 0.85rem; + border: 1px solid var(--border); + background: rgba(24, 31, 40, 0.9); + color: var(--text-main); +} + +input:focus, +select:focus, +button:focus-visible { + outline: 2px solid rgba(104, 196, 255, 0.34); + outline-offset: 2px; +} + +.summary { + padding: 1.25rem; + display: grid; + gap: 0.8rem; +} + +.summary-row { + display: flex; + justify-content: space-between; + gap: 1rem; + padding-bottom: 0.8rem; + border-bottom: 1px solid var(--border); +} + +.summary-row span { + color: var(--text-muted); +} + +.summary-row strong { + text-align: right; +} + +.notice { + margin-top: 1rem; + padding: 0.85rem 1rem; + color: var(--danger); + background: var(--danger-bg); + border: 1px solid rgba(255, 107, 107, 0.38); +} + +.actions { + padding: 1rem 1.5rem; + display: flex; + justify-content: flex-end; + gap: 0.75rem; + border-top: 1px solid var(--border); + background: linear-gradient(90deg, rgba(11, 17, 24, 0.82), rgba(23, 31, 40, 0.86)); +} + +.btn { + min-height: 2.75rem; + padding: 0.72rem 1rem; + border: 1px solid var(--border-strong); + background: rgba(24, 31, 40, 0.9); + color: var(--text-main); + font-size: 0.82rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.btn.primary { + background: rgba(24, 86, 126, 0.95); + color: #ffffff; + border-color: rgba(104, 196, 255, 0.34); +} + +.btn.primary:hover { + background: rgba(32, 108, 156, 0.98); +} + +.btn.secondary { + background: rgba(255, 255, 255, 0.03); + color: var(--text-main); +} + +.btn.secondary:hover { + background: var(--accent-soft); + border-color: rgba(104, 196, 255, 0.28); +} + +.close:hover { + background: rgba(255, 96, 96, 0.2); + border-color: rgba(255, 96, 96, 0.35); +} + +@media (max-width: 900px) { + .grid, + .form { + grid-template-columns: 1fr; + } +} diff --git a/arma/forge_pmc_simulator.Tanoa/ui/_site/mission-setup.js b/arma/forge_pmc_simulator.Tanoa/ui/_site/mission-setup.js new file mode 100644 index 0000000..1b7e124 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/ui/_site/mission-setup.js @@ -0,0 +1,323 @@ +(function () { + //#region Types and state + + /** + * @typedef {Object} MissionSettings + * @property {string} enemyFaction + * @property {number} maxConcurrentMissions + * @property {number} missionInterval + * @property {number} moneyMin + * @property {number} moneyMax + * @property {number} reputationMin + * @property {number} reputationMax + * @property {number} penaltyMin + * @property {number} penaltyMax + * @property {number} timeLimitMin + * @property {number} timeLimitMax + */ + + /** + * @typedef {Object} FactionOption + * @property {string} faction - Faction classname. + * @property {string} display - User-facing faction name. + * @property {number} value - Original mission parameter value. + */ + + /** + * Local UI state. SQF hydrates this with authoritative defaults and + * faction options after the browser control reports ready. + * + * @type {{factions: FactionOption[], settings: MissionSettings, error: string}} + */ + const state = { + factions: [], + settings: { + enemyFaction: "IND_G_F", + maxConcurrentMissions: 3, + missionInterval: 300, + moneyMin: 25000, + moneyMax: 60000, + reputationMin: 6, + reputationMax: 14, + penaltyMin: -8, + penaltyMax: -3, + timeLimitMin: 900, + timeLimitMax: 1800, + }, + error: "", + }; + + //#endregion + + //#region Arma bridge helpers + + /** + * Sends a bridge event to SQF through the Arma browser API. + * + * @param {string} event - Mission setup bridge event name. + * @param {Record} [data={}] - Event payload. + * @returns {boolean} True when the event was sent to Arma. + */ + function send(event, data = {}) { + if (!window.A3API || typeof window.A3API.SendAlert !== "function") { + return false; + } + + window.A3API.SendAlert(JSON.stringify({ event, data })); + return true; + } + + //#endregion + + //#region Form state and validation + + /** + * Reads a numeric input value and normalizes invalid values to zero. + * + * @param {string} id - DOM id of the numeric input. + * @returns {number} Parsed finite number, or zero. + */ + function fieldNumber(id) { + const value = Number(document.getElementById(id)?.value || 0); + return Number.isFinite(value) ? value : 0; + } + + /** + * Reads current form values into the payload expected by + * forge_pmc_fnc_setupMenu_applySettings. + * + * @returns {MissionSettings} Current mission setup values. + */ + function readSettings() { + return { + enemyFaction: String(document.getElementById("enemyFaction")?.value || "IND_G_F"), + maxConcurrentMissions: fieldNumber("maxConcurrentMissions"), + missionInterval: fieldNumber("missionInterval"), + moneyMin: fieldNumber("moneyMin"), + moneyMax: fieldNumber("moneyMax"), + reputationMin: fieldNumber("reputationMin"), + reputationMax: fieldNumber("reputationMax"), + penaltyMin: fieldNumber("penaltyMin"), + penaltyMax: fieldNumber("penaltyMax"), + timeLimitMin: fieldNumber("timeLimitMin"), + timeLimitMax: fieldNumber("timeLimitMax"), + }; + } + + /** + * Validates and submits UI settings to SQF. + * + * @returns {void} + */ + function apply() { + const settings = readSettings(); + if (settings.moneyMax < settings.moneyMin) { + state.error = "Money max must be greater than or equal to money min."; + render(); + return; + } + + if (settings.reputationMax < settings.reputationMin) { + state.error = "Reputation max must be greater than or equal to reputation min."; + render(); + return; + } + + if (settings.timeLimitMax < settings.timeLimitMin) { + state.error = "Time limit max must be greater than or equal to time limit min."; + render(); + return; + } + + state.error = ""; + send("missionSetup::apply", settings); + } + + /** + * Cancels UI overrides and tells SQF to apply mission params/defaults now. + * + * @returns {void} + */ + function close() { + send("missionSetup::cancel", {}); + } + + //#endregion + + //#region Rendering + + /** + * Renders a faction ``; + } + + /** + * Renders the full setup UI and binds control events. + * + * @returns {void} + */ + function render() { + const settings = state.settings; + const faction = state.factions.find((item) => item.faction === settings.enemyFaction); + const factionLabel = faction ? faction.display : settings.enemyFaction; + + document.getElementById("app").innerHTML = ` +
+
+
+ FORGE ORBIS + PMC Simulator Setup +
+ +
+ +
+
+
+
+ Deployment Profile +

Operation Settings

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + +
+
+ `; + + // DOM nodes are replaced on each render, so event handlers are rebound. + document.querySelectorAll("input, select").forEach((input) => { + input.addEventListener("change", () => { + state.settings = readSettings(); + render(); + }); + }); + + document.querySelectorAll("[data-action='close']").forEach((button) => { + button.addEventListener("click", close); + }); + + document.querySelector("[data-action='apply']").addEventListener("click", apply); + } + + //#endregion + + //#region SQF inbound bridge + + /** + * Bridge object called by SQF through ExecJS. + * + * @type {{receive: (payload: {event?: string, data?: Record}) => boolean}} + */ + window.MissionSetupBridge = { + /** + * Receives hydration and error events from SQF. + * + * @param {{event?: string, data?: Record}} payload - SQF bridge payload. + * @returns {boolean} True when the event was handled. + */ + receive(payload) { + if (!payload || typeof payload !== "object") { + return false; + } + + if (payload.event === "missionSetup::hydrate") { + state.factions = Array.isArray(payload.data?.factions) ? payload.data.factions : []; + state.settings = Object.assign({}, state.settings, payload.data?.settings || {}); + render(); + return true; + } + + if (payload.event === "missionSetup::error") { + state.error = String(payload.data?.message || "Mission setup failed."); + render(); + return true; + } + + return false; + }, + }; + + //#endregion + + //#region Bootstrap + + // Render local defaults immediately, then request authoritative SQF data. + render(); + send("missionSetup::ready", { loaded: true }); + + //#endregion +})(); diff --git a/arma/forge_pmc_simulator.Tanoa/ui/baseControls.hpp b/arma/forge_pmc_simulator.Tanoa/ui/baseControls.hpp new file mode 100644 index 0000000..74c6a96 --- /dev/null +++ b/arma/forge_pmc_simulator.Tanoa/ui/baseControls.hpp @@ -0,0 +1,1840 @@ +// Generated by: "Default" call BIS_fnc_exportGUIBaseClasses; + +// Control types +#define CT_STATIC 0 +#define CT_BUTTON 1 +#define CT_EDIT 2 +#define CT_SLIDER 3 +#define CT_COMBO 4 +#define CT_LISTBOX 5 +#define CT_TOOLBOX 6 +#define CT_CHECKBOXES 7 +#define CT_PROGRESS 8 +#define CT_HTML 9 +#define CT_STATIC_SKEW 10 +#define CT_ACTIVETEXT 11 +#define CT_TREE 12 +#define CT_STRUCTURED_TEXT 13 +#define CT_CONTEXT_MENU 14 +#define CT_CONTROLS_GROUP 15 +#define CT_SHORTCUTBUTTON 16 +#define CT_HITZONES 17 +#define CT_XKEYDESC 40 +#define CT_XBUTTON 41 +#define CT_XLISTBOX 42 +#define CT_XSLIDER 43 +#define CT_XCOMBO 44 +#define CT_ANIMATED_TEXTURE 45 +#define CT_OBJECT 80 +#define CT_OBJECT_ZOOM 81 +#define CT_OBJECT_CONTAINER 82 +#define CT_OBJECT_CONT_ANIM 83 +#define CT_LINEBREAK 98 +#define CT_USER 99 +#define CT_MAP 100 +#define CT_MAP_MAIN 101 +#define CT_LISTNBOX 102 +#define CT_ITEMSLOT 103 +#define CT_CHECKBOX 77 + +// Static styles +#define ST_POS 0x0F +#define ST_HPOS 0x03 +#define ST_VPOS 0x0C +#define ST_LEFT 0x00 +#define ST_RIGHT 0x01 +#define ST_CENTER 0x02 +#define ST_DOWN 0x04 +#define ST_UP 0x08 +#define ST_VCENTER 0x0C + +#define ST_TYPE 0xF0 +#define ST_SINGLE 0x00 +#define ST_MULTI 0x10 +#define ST_TITLE_BAR 0x20 +#define ST_PICTURE 0x30 +#define ST_FRAME 0x40 +#define ST_BACKGROUND 0x50 +#define ST_GROUP_BOX 0x60 +#define ST_GROUP_BOX2 0x70 +#define ST_HUD_BACKGROUND 0x80 +#define ST_TILE_PICTURE 0x90 +#define ST_WITH_RECT 0xA0 +#define ST_LINE 0xB0 +#define ST_UPPERCASE 0xC0 +#define ST_LOWERCASE 0xD0 + +#define ST_SHADOW 0x100 +#define ST_NO_RECT 0x200 +#define ST_KEEP_ASPECT_RATIO 0x800 + +// Slider styles +#define SL_DIR 0x400 +#define SL_VERT 0 +#define SL_HORZ 0x400 + +#define SL_TEXTURES 0x10 + +// progress bar +#define ST_VERTICAL 0x01 +#define ST_HORIZONTAL 0 + +// Listbox styles +#define LB_TEXTURES 0x10 +#define LB_MULTI 0x20 + +// Tree styles +#define TR_SHOWROOT 1 +#define TR_AUTOCOLLAPSE 2 + +// Default grid +#define GUI_GRID_WAbs ((safezoneW / safezoneH) min 1.2) +#define GUI_GRID_HAbs (GUI_GRID_WAbs / 1.2) +#define GUI_GRID_W (GUI_GRID_WAbs / 40) +#define GUI_GRID_H (GUI_GRID_HAbs / 25) +#define GUI_GRID_X (safezoneX) +#define GUI_GRID_Y (safezoneY + safezoneH - GUI_GRID_HAbs) + +// Default text sizes +#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8) +#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1) +#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2) + +// Pixel grid +#define pixelScale 0.50 +#define GRID_W (pixelW * pixelGrid * pixelScale) +#define GRID_H (pixelH * pixelGrid * pixelScale) + + +class ScrollBar +{ + color[] = {1,1,1,0.6}; + colorActive[] = {1,1,1,1}; + colorDisabled[] = {1,1,1,0.3}; + thumb = "\A3\ui_f\data\gui\cfg\scrollbar\thumb_ca.paa"; + arrowEmpty = "\A3\ui_f\data\gui\cfg\scrollbar\arrowEmpty_ca.paa"; + arrowFull = "\A3\ui_f\data\gui\cfg\scrollbar\arrowFull_ca.paa"; + border = "\A3\ui_f\data\gui\cfg\scrollbar\border_ca.paa"; + shadow = 0; + scrollSpeed = 0.06; + width = 0; + height = 0; + autoScrollEnabled = 0; + autoScrollSpeed = -1; + autoScrollDelay = 5; + autoScrollRewind = 0; +}; +class RscObject +{ + access = 0; + type = CT_OBJECT; + scale = 1; + direction[] = {0,0,1}; + model = "\core\empty\empty.p3d"; + up[] = {0,1,0}; + shadow = 0; +}; +class RscText +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_STATIC; + idc = -1; + colorBackground[] = {0,0,0,0}; + colorText[] = {1,1,1,1}; + text = ""; + fixedWidth = 0; + x = 0; + y = 0; + h = 0.037; + w = 0.3; + style = ST_LEFT; + shadow = 1; + colorShadow[] = {0,0,0,0.5}; + font = "RobotoCondensed"; + SizeEx = GUI_TEXT_SIZE_MEDIUM; + linespacing = 1; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; +}; +class RscFrame +{ + type = CT_STATIC; + idc = -1; + deletable = 0; + style = ST_FRAME; + shadow = 2; + colorBackground[] = {0,0,0,0}; + colorText[] = {1,1,1,1}; + font = "RobotoCondensed"; + sizeEx = 0.02; + text = ""; + x = 0; + y = 0; + w = 0.3; + h = 0.3; +}; +class RscLine: RscText +{ + idc = -1; + style = ST_MULTI + ST_TITLE_BAR + ST_HUD_BACKGROUND; + x = 0.17; + y = 0.48; + w = 0.66; + h = 0; + text = ""; + colorBackground[] = {0,0,0,0}; + colorText[] = {1,1,1,1}; +}; +class RscProgress +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_PROGRESS; + style = ST_HORIZONTAL; + colorFrame[] = {0,0,0,0}; + colorBar[] = + { + "(profilenamespace getvariable ['GUI_BCG_RGB_R',0.13])", + "(profilenamespace getvariable ['GUI_BCG_RGB_G',0.54])", + "(profilenamespace getvariable ['GUI_BCG_RGB_B',0.21])", + "(profilenamespace getvariable ['GUI_BCG_RGB_A',0.8])" + }; + x = 0.344; + y = 0.619; + w = 0.313726; + h = 0.0261438; + shadow = 2; + texture = "#(argb,8,8,3)color(1,1,1,1)"; +}; +class RscPicture +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_STATIC; + idc = -1; + style = ST_MULTI + ST_TITLE_BAR; + colorBackground[] = {0,0,0,0}; + colorText[] = {1,1,1,1}; + font = "TahomaB"; + sizeEx = 0; + lineSpacing = 0; + text = ""; + fixedWidth = 0; + shadow = 0; + x = 0; + y = 0; + w = 0.2; + h = 0.15; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; +}; +class RscPictureKeepAspect: RscPicture +{ + style = ST_MULTI + ST_TITLE_BAR + ST_KEEP_ASPECT_RATIO; +}; +class RscVideo: RscPicture +{ + autoplay = 1; + loops = 1; +}; +class RscHTML +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_HTML; + idc = -1; + style = ST_LEFT; + filename = ""; + colorBackground[] = {0,0,0,0}; + colorText[] = {1,1,1,1}; + colorBold[] = {1,1,1,1}; + colorLink[] = {1,1,1,0.75}; + colorLinkActive[] = {1,1,1,1}; + colorPicture[] = {1,1,1,1}; + colorPictureLink[] = {1,1,1,1}; + colorPictureSelected[] = {1,1,1,1}; + colorPictureBorder[] = {0,0,0,0}; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; + class H1 + { + font = "RobotoCondensed"; + fontBold = "RobotoCondensedBold"; + sizeEx = GUI_TEXT_SIZE_LARGE; + align = "left"; + }; + class H2 + { + font = "RobotoCondensed"; + fontBold = "RobotoCondensedBold"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + align = "right"; + }; + class H3 + { + font = "RobotoCondensed"; + fontBold = "RobotoCondensedBold"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + align = "left"; + }; + class H4 + { + font = "RobotoCondensed"; + fontBold = "RobotoCondensedBold"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + align = "left"; + }; + class H5 + { + font = "RobotoCondensed"; + fontBold = "RobotoCondensedBold"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + align = "left"; + }; + class H6 + { + font = "RobotoCondensed"; + fontBold = "RobotoCondensedBold"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + align = "left"; + }; + class P + { + font = "RobotoCondensed"; + fontBold = "RobotoCondensedBold"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + align = "left"; + }; + x = 0; + y = 0; + w = 0.1; + h = 0.1; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + prevPage = "\A3\ui_f\data\gui\rsccommon\rschtml\arrow_left_ca.paa"; + nextPage = "\A3\ui_f\data\gui\rsccommon\rschtml\arrow_right_ca.paa"; + shadow = 2; +}; +class RscButton +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_BUTTON; + text = ""; + colorText[] = {1,1,1,1}; + colorDisabled[] = {1,1,1,0.25}; + colorBackground[] = {0,0,0,0.5}; + colorBackgroundDisabled[] = {0,0,0,0.5}; + colorBackgroundActive[] = {0,0,0,1}; + colorFocused[] = {0,0,0,1}; + colorShadow[] = {0,0,0,0}; + colorBorder[] = {0,0,0,1}; + soundEnter[] = + { + "\A3\ui_f\data\sound\RscButton\soundEnter", + 0.09, + 1 + }; + soundPush[] = + { + "\A3\ui_f\data\sound\RscButton\soundPush", + 0.09, + 1 + }; + soundClick[] = + { + "\A3\ui_f\data\sound\RscButton\soundClick", + 0.09, + 1 + }; + soundEscape[] = + { + "\A3\ui_f\data\sound\RscButton\soundEscape", + 0.09, + 1 + }; + idc = -1; + style = ST_CENTER; + x = 0; + y = 0; + w = 0.095589; + h = 0.039216; + shadow = 2; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + url = ""; + offsetX = 0; + offsetY = 0; + offsetPressedX = 0; + offsetPressedY = 0; + borderSize = 0; +}; +class RscShortcutButton +{ + deletable = 0; + fade = 0; + type = CT_SHORTCUTBUTTON; + x = 0.1; + y = 0.1; + class HitZone + { + left = 0; + top = 0; + right = 0; + bottom = 0; + }; + class ShortcutPos + { + left = 0; + top = ((GUI_GRID_HAbs / 20) - GUI_TEXT_SIZE_MEDIUM) / 2; + w = GUI_TEXT_SIZE_MEDIUM * (3/4); + h = GUI_TEXT_SIZE_MEDIUM; + }; + class TextPos + { + left = GUI_TEXT_SIZE_MEDIUM * (3/4); + top = ((GUI_GRID_HAbs / 20) - GUI_TEXT_SIZE_MEDIUM) / 2; + right = 0.005; + bottom = 0; + }; + shortcuts[] = {}; + textureNoShortcut = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {1,1,1,1}; + colorFocused[] = {1,1,1,1}; + color2[] = {0.95,0.95,0.95,1}; + colorDisabled[] = {1,1,1,0.25}; + colorBackground[] = + { + "(profilenamespace getvariable ['GUI_BCG_RGB_R',0.13])", + "(profilenamespace getvariable ['GUI_BCG_RGB_G',0.54])", + "(profilenamespace getvariable ['GUI_BCG_RGB_B',0.21])", + 1 + }; + colorBackgroundFocused[] = + { + "(profilenamespace getvariable ['GUI_BCG_RGB_R',0.13])", + "(profilenamespace getvariable ['GUI_BCG_RGB_G',0.54])", + "(profilenamespace getvariable ['GUI_BCG_RGB_B',0.21])", + 1 + }; + colorBackground2[] = {1,1,1,1}; + soundEnter[] = + { + "\A3\ui_f\data\sound\RscButton\soundEnter", + 0.09, + 1 + }; + soundPush[] = + { + "\A3\ui_f\data\sound\RscButton\soundPush", + 0.09, + 1 + }; + soundClick[] = + { + "\A3\ui_f\data\sound\RscButton\soundClick", + 0.09, + 1 + }; + soundEscape[] = + { + "\A3\ui_f\data\sound\RscButton\soundEscape", + 0.09, + 1 + }; + class Attributes + { + font = "RobotoCondensed"; + color = "#E5E5E5"; + align = "left"; + shadow = "true"; + }; + idc = -1; + style = ST_LEFT; + default = 0; + shadow = 1; + w = 0.183825; + h = (GUI_GRID_HAbs / 20); + textSecondary = ""; + colorSecondary[] = {1,1,1,1}; + colorFocusedSecondary[] = {1,1,1,1}; + color2Secondary[] = {0.95,0.95,0.95,1}; + colorDisabledSecondary[] = {1,1,1,0.25}; + sizeExSecondary = GUI_TEXT_SIZE_MEDIUM; + fontSecondary = "RobotoCondensed"; + animTextureDefault = "\A3\ui_f\data\GUI\RscCommon\RscShortcutButton\normal_ca.paa"; + animTextureNormal = "\A3\ui_f\data\GUI\RscCommon\RscShortcutButton\normal_ca.paa"; + animTextureDisabled = "\A3\ui_f\data\GUI\RscCommon\RscShortcutButton\normal_ca.paa"; + animTextureOver = "\A3\ui_f\data\GUI\RscCommon\RscShortcutButton\over_ca.paa"; + animTextureFocused = "\A3\ui_f\data\GUI\RscCommon\RscShortcutButton\focus_ca.paa"; + animTexturePressed = "\A3\ui_f\data\GUI\RscCommon\RscShortcutButton\down_ca.paa"; + periodFocus = 1.2; + periodOver = 0.8; + period = 0.4; + font = "RobotoCondensed"; + size = GUI_TEXT_SIZE_MEDIUM; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + text = ""; + url = ""; + action = ""; + class AttributesImage + { + font = "RobotoCondensed"; + color = "#E5E5E5"; + align = "left"; + }; +}; +class RscEdit +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_EDIT; + x = 0; + y = 0; + h = 0.04; + w = 0.2; + colorBackground[] = {0,0,0,0}; + colorText[] = {0.95,0.95,0.95,1}; + colorDisabled[] = {1,1,1,0.25}; + colorSelection[] = + { + "(profilenamespace getvariable ['GUI_BCG_RGB_R',0.13])", + "(profilenamespace getvariable ['GUI_BCG_RGB_G',0.54])", + "(profilenamespace getvariable ['GUI_BCG_RGB_B',0.21])", + 1 + }; + autocomplete = ""; + text = ""; + size = 0.2; + style = ST_FRAME; + font = "RobotoCondensed"; + shadow = 2; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + canModify = 1; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; + onChar = "call DBUG_fnc_charEnter"; +}; +class RscCombo +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_COMBO; + colorSelect[] = {0,0,0,1}; + colorText[] = {1,1,1,1}; + colorBackground[] = {0,0,0,1}; + colorScrollbar[] = {1,0,0,1}; + colorDisabled[] = {1,1,1,0.25}; + colorPicture[] = {1,1,1,1}; + colorPictureSelected[] = {1,1,1,1}; + colorPictureDisabled[] = {1,1,1,0.25}; + colorPictureRight[] = {1,1,1,1}; + colorPictureRightSelected[] = {1,1,1,1}; + colorPictureRightDisabled[] = {1,1,1,0.25}; + colorTextRight[] = {1,1,1,1}; + colorSelectRight[] = {0,0,0,1}; + colorSelect2Right[] = {0,0,0,1}; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; + soundSelect[] = + { + "\A3\ui_f\data\sound\RscCombo\soundSelect", + 0.1, + 1 + }; + soundExpand[] = + { + "\A3\ui_f\data\sound\RscCombo\soundExpand", + 0.1, + 1 + }; + soundCollapse[] = + { + "\A3\ui_f\data\sound\RscCombo\soundCollapse", + 0.1, + 1 + }; + maxHistoryDelay = 1; + class ComboScrollBar: ScrollBar + { + color[] = {1,1,1,1}; + }; + style = ST_MULTI + ST_NO_RECT; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + shadow = 0; + x = 0; + y = 0; + w = 0.12; + h = 0.035; + colorSelectBackground[] = {1,1,1,0.7}; + arrowEmpty = "\A3\ui_f\data\GUI\RscCommon\rsccombo\arrow_combo_ca.paa"; + arrowFull = "\A3\ui_f\data\GUI\RscCommon\rsccombo\arrow_combo_active_ca.paa"; + wholeHeight = 0.45; + colorActive[] = {1,0,0,1}; +}; +class RscListBox +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_LISTBOX; + rowHeight = 0; + colorText[] = {1,1,1,1}; + colorDisabled[] = {1,1,1,0.25}; + colorScrollbar[] = {1,0,0,0}; + colorSelect[] = {0,0,0,1}; + colorSelect2[] = {0,0,0,1}; + colorSelectBackground[] = {0.95,0.95,0.95,1}; + colorSelectBackground2[] = {1,1,1,0.5}; + colorBackground[] = {0,0,0,0.3}; + soundSelect[] = + { + "\A3\ui_f\data\sound\RscListbox\soundSelect", + 0.09, + 1 + }; + autoScrollSpeed = -1; + autoScrollDelay = 5; + autoScrollRewind = 0; + arrowEmpty = "#(argb,8,8,3)color(1,1,1,1)"; + arrowFull = "#(argb,8,8,3)color(1,1,1,1)"; + colorPicture[] = {1,1,1,1}; + colorPictureSelected[] = {1,1,1,1}; + colorPictureDisabled[] = {1,1,1,0.25}; + colorPictureRight[] = {1,1,1,1}; + colorPictureRightSelected[] = {1,1,1,1}; + colorPictureRightDisabled[] = {1,1,1,0.25}; + colorTextRight[] = {1,1,1,1}; + colorSelectRight[] = {0,0,0,1}; + colorSelect2Right[] = {0,0,0,1}; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; + class ListScrollBar: ScrollBar + { + color[] = {1,1,1,1}; + autoScrollEnabled = 1; + }; + x = 0; + y = 0; + w = 0.3; + h = 0.3; + style = LB_TEXTURES; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + shadow = 0; + colorShadow[] = {0,0,0,0.5}; + period = 1.2; + maxHistoryDelay = 1; +}; +class RscListNBox +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_LISTNBOX; + rowHeight = 0; + colorText[] = {1,1,1,1}; + colorScrollbar[] = {0.95,0.95,0.95,1}; + colorSelect[] = {0,0,0,1}; + colorSelect2[] = {0,0,0,1}; + colorSelectBackground[] = {0.95,0.95,0.95,1}; + colorSelectBackground2[] = {1,1,1,0.5}; + colorBackground[] = {0,0,0,1}; + maxHistoryDelay = 1; + soundSelect[] = + { + "", + 0.1, + 1 + }; + autoScrollSpeed = -1; + autoScrollDelay = 5; + autoScrollRewind = 0; + arrowEmpty = "#(argb,8,8,3)color(1,1,1,1)"; + arrowFull = "#(argb,8,8,3)color(1,1,1,1)"; + drawSideArrows = 0; + columns[] = {0.3,0.6,0.7}; + idcLeft = -1; + idcRight = -1; + class ListScrollBar: ScrollBar + { + }; + style = ST_MULTI; + shadow = 0; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + color[] = {0.95,0.95,0.95,1}; + colorDisabled[] = {1,1,1,0.25}; + colorPicture[] = {1,1,1,1}; + colorPictureSelected[] = {1,1,1,1}; + colorPictureDisabled[] = {1,1,1,1}; + period = 1.2; + x = 0; + y = 0; + w = 0.3; + h = 0.3; + class ScrollBar: ScrollBar + { + }; +}; +class RscXListBox +{ + deletable = 0; + fade = 0; + idc = -1; + type = CT_XLISTBOX; + x = 0.1; + y = 0.1; + color[] = {1,1,1,0.6}; + colorActive[] = {1,1,1,1}; + colorDisabled[] = {1,1,1,0.25}; + colorSelect[] = {0.95,0.95,0.95,1}; + colorText[] = {1,1,1,1}; + soundSelect[] = + { + "\A3\ui_f\data\sound\RscListbox\soundSelect", + 0.09, + 1 + }; + colorPicture[] = {1,1,1,1}; + colorPictureSelected[] = {1,1,1,1}; + colorPictureDisabled[] = {1,1,1,0.25}; + colorPictureRight[] = {1,1,1,1}; + colorPictureRightSelected[] = {1,1,1,1}; + colorPictureRightDisabled[] = {1,1,1,0.25}; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; + style = ST_CENTER + LB_TEXTURES + SL_HORZ; + shadow = 2; + arrowEmpty = "\A3\ui_f\data\gui\cfg\slider\arrowEmpty_ca.paa"; + arrowFull = "\A3\ui_f\data\gui\cfg\slider\arrowFull_ca.paa"; + border = "\A3\ui_f\data\gui\cfg\slider\border_ca.paa"; + w = 0.14706; + h = 0.039216; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; +}; +class RscTree +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_TREE; + colorBackground[] = {0,0,0,0}; + colorSelect[] = {1,1,1,0.7}; + colorDisabled[] = {1,1,1,0.25}; + colorText[] = {1,1,1,1}; + colorSelectText[] = {0,0,0,1}; + colorBorder[] = {0,0,0,0}; + colorSearch[] = + { + "(profilenamespace getvariable ['GUI_BCG_RGB_R',0.13])", + "(profilenamespace getvariable ['GUI_BCG_RGB_G',0.54])", + "(profilenamespace getvariable ['GUI_BCG_RGB_B',0.21])", + "(profilenamespace getvariable ['GUI_BCG_RGB_A',0.8])" + }; + colorMarked[] = {0.2,0.3,0.7,1}; + colorMarkedText[] = {0,0,0,1}; + colorMarkedSelected[] = {0,0.5,0.5,1}; + multiselectEnabled = 0; + colorPicture[] = {1,1,1,1}; + colorPictureSelected[] = {0,0,0,1}; + colorPictureDisabled[] = {1,1,1,0.25}; + colorPictureRight[] = {1,1,1,1}; + colorPictureRightSelected[] = {0,0,0,1}; + colorPictureRightDisabled[] = {1,1,1,0.25}; + colorArrow[] = {1,1,1,1}; + maxHistoryDelay = 1; + shadow = 0; + style = ST_LEFT; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + expandedTexture = "A3\ui_f\data\gui\rsccommon\rsctree\expandedTexture_ca.paa"; + hiddenTexture = "A3\ui_f\data\gui\rsccommon\rsctree\hiddenTexture_ca.paa"; + x = 0; + y = 0; + w = 0.1; + h = 0.2; + rowHeight = 0.0439091; + colorSelectBackground[] = {0,0,0,0.5}; + colorLines[] = {0,0,0,0}; + borderSize = 0; + expandOnDoubleclick = 1; + class ScrollBar: ScrollBar + { + }; +}; +class RscSlider +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_SLIDER; + style = SL_HORZ; + color[] = {1,1,1,0.8}; + colorActive[] = {1,1,1,1}; + shadow = 0; + x = 0; + y = 0; + w = 0.3; + h = 0.025; +}; +class RscXSliderH +{ + deletable = 0; + fade = 0; + type = CT_XSLIDER; + color[] = {1,1,1,0.6}; + colorActive[] = {1,1,1,1}; + colorDisable[] = {1,1,1,0.4}; + style = SL_TEXTURES + SL_HORZ; + shadow = 0; + x = 0; + y = 0; + h = 0.029412; + w = 0.4; + colorDisabled[] = {1,1,1,0.2}; + arrowEmpty = "\A3\ui_f\data\gui\cfg\slider\arrowEmpty_ca.paa"; + arrowFull = "\A3\ui_f\data\gui\cfg\slider\arrowFull_ca.paa"; + border = "\A3\ui_f\data\gui\cfg\slider\border_ca.paa"; + thumb = "\A3\ui_f\data\gui\cfg\slider\thumb_ca.paa"; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; +}; +class RscActiveText +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_ACTIVETEXT; + style = ST_CENTER; + color[] = {0,0,0,1}; + colorActive[] = {0.3,0.4,0,1}; + colorDisabled[] = {1,1,1,0.25}; + soundEnter[] = + { + "", + 0.1, + 1 + }; + soundPush[] = + { + "", + 0.1, + 1 + }; + soundClick[] = + { + "", + 0.1, + 1 + }; + soundEscape[] = + { + "", + 0.1, + 1 + }; + text = ""; + default = 0; + idc = -1; + x = 0; + y = 0; + h = 0.035; + w = 0.035; + font = "RobotoCondensed"; + shadow = 2; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + url = ""; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; +}; +class RscActivePicture: RscActiveText +{ + style = ST_MULTI + ST_TITLE_BAR; + color[] = {1,1,1,0.5}; + colorActive[] = {1,1,1,1}; +}; +class RscActivePictureKeepAspect: RscActivePicture +{ + style = ST_MULTI + ST_TITLE_BAR + ST_KEEP_ASPECT_RATIO; +}; +class RscStructuredText +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_STRUCTURED_TEXT; + idc = -1; + style = ST_LEFT; + colorText[] = {1,1,1,1}; + class Attributes + { + font = "RobotoCondensed"; + color = "#ffffff"; + colorLink = "#D09B43"; + align = "left"; + shadow = 1; + }; + x = 0; + y = 0; + h = 0.035; + w = 0.1; + text = ""; + size = GUI_TEXT_SIZE_MEDIUM; + shadow = 1; +}; +class RscToolbox +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_TOOLBOX; + style = ST_CENTER; + colorText[] = {0.95,0.95,0.95,1}; + color[] = {0.95,0.95,0.95,1}; + colorTextSelect[] = {0.95,0.95,0.95,1}; + colorSelect[] = {0.95,0.95,0.95,1}; + colorTextDisable[] = {0.4,0.4,0.4,1}; + colorDisable[] = {0.4,0.4,0.4,1}; + colorSelectedBg[] = + { + "(profilenamespace getvariable ['GUI_BCG_RGB_R',0.13])", + "(profilenamespace getvariable ['GUI_BCG_RGB_G',0.54])", + "(profilenamespace getvariable ['GUI_BCG_RGB_B',0.21])", + 0.5 + }; + shadow = 0; + strings[] = + { + "", + "" + }; + x = 0; + y = 0; + w = 0.3; + h = 0.025; + rows = 1; + columns = 2; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_SMALL; +}; +class RscControlsGroup +{ + deletable = 0; + fade = 0; + class VScrollbar: ScrollBar + { + color[] = {1,1,1,1}; + width = 0.021; + autoScrollEnabled = 1; + }; + class HScrollbar: ScrollBar + { + color[] = {1,1,1,1}; + height = 0.028; + }; + class Controls + { + }; + type = CT_CONTROLS_GROUP; + idc = -1; + x = 0; + y = 0; + w = 1; + h = 1; + shadow = 0; + style = ST_MULTI; +}; +class RscControlsGroupNoScrollbars: RscControlsGroup +{ + class VScrollbar: VScrollbar + { + width = 0; + }; + class HScrollbar: HScrollbar + { + height = 0; + }; +}; +class RscControlsGroupNoHScrollbars: RscControlsGroup +{ + class HScrollbar: HScrollbar + { + height = 0; + }; +}; +class RscControlsGroupNoVScrollbars: RscControlsGroup +{ + class VScrollbar: VScrollbar + { + width = 0; + }; +}; +class RscButtonTextOnly: RscButton +{ + SizeEx = GUI_TEXT_SIZE_SMALL; + colorBackground[] = {1,1,1,0}; + colorBackgroundActive[] = {1,1,1,0}; + colorBackgroundDisabled[] = {1,1,1,0}; + colorFocused[] = {1,1,1,0}; + colorShadow[] = {1,1,1,0}; + borderSize = 0; +}; +class RscButtonMenu: RscShortcutButton +{ + idc = -1; + type = CT_SHORTCUTBUTTON; + style = ST_CENTER + ST_FRAME + ST_HUD_BACKGROUND; + default = 0; + shadow = 0; + x = 0; + y = 0; + w = 0.095589; + h = 0.039216; + animTextureNormal = "#(argb,8,8,3)color(1,1,1,1)"; + animTextureDisabled = "#(argb,8,8,3)color(1,1,1,1)"; + animTextureOver = "#(argb,8,8,3)color(1,1,1,1)"; + animTextureFocused = "#(argb,8,8,3)color(1,1,1,1)"; + animTexturePressed = "#(argb,8,8,3)color(1,1,1,1)"; + animTextureDefault = "#(argb,8,8,3)color(1,1,1,1)"; + colorBackground[] = {0,0,0,0.8}; + colorBackgroundFocused[] = {1,1,1,1}; + colorBackground2[] = {0.75,0.75,0.75,1}; + color[] = {1,1,1,1}; + colorFocused[] = {0,0,0,1}; + color2[] = {0,0,0,1}; + colorText[] = {1,1,1,1}; + colorDisabled[] = {1,1,1,0.25}; + textSecondary = ""; + colorSecondary[] = {1,1,1,1}; + colorFocusedSecondary[] = {0,0,0,1}; + color2Secondary[] = {0,0,0,1}; + colorDisabledSecondary[] = {1,1,1,0.25}; + sizeExSecondary = GUI_TEXT_SIZE_MEDIUM; + fontSecondary = "PuristaLight"; + period = 1.2; + periodFocus = 1.2; + periodOver = 1.2; + size = GUI_TEXT_SIZE_MEDIUM; + sizeEx = GUI_TEXT_SIZE_MEDIUM; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; + class TextPos + { + left = 0.25 * GUI_GRID_W; + top = (GUI_GRID_H - GUI_TEXT_SIZE_MEDIUM) / 2; + right = 0.005; + bottom = 0; + }; + class Attributes + { + font = "PuristaLight"; + color = "#E5E5E5"; + align = "left"; + shadow = "false"; + }; + class ShortcutPos + { + left = 5.25 * GUI_GRID_W; + top = 0; + w = 1 * GUI_GRID_W; + h = 1 * GUI_GRID_H; + }; + soundEnter[] = + { + "\A3\ui_f\data\sound\RscButtonMenu\soundEnter", + 0.09, + 1 + }; + soundPush[] = + { + "\A3\ui_f\data\sound\RscButtonMenu\soundPush", + 0.09, + 1 + }; + soundClick[] = + { + "\A3\ui_f\data\sound\RscButtonMenu\soundClick", + 0.09, + 1 + }; + soundEscape[] = + { + "\A3\ui_f\data\sound\RscButtonMenu\soundEscape", + 0.09, + 1 + }; +}; +class RscButtonMenuOK: RscButtonMenu +{ + idc = 1; + shortcuts[] = + { + "0x00050000 + 0", + 28, + 57, + 156 + }; + default = 1; + text = "OK"; + soundPush[] = + { + "\A3\ui_f\data\sound\RscButtonMenuOK\soundPush", + 0.09, + 1 + }; +}; +class RscButtonMenuCancel: RscButtonMenu +{ + idc = 2; + shortcuts[] = + { + "0x00050000 + 1" + }; + text = "Cancel"; +}; +class RscButtonMenuSteam: RscButtonMenu +{ + colorBackground[] = {0.0313726,0.721569,0.917647,1}; + textureNoShortcut = "\A3\Ui_f\data\GUI\RscCommon\RscButtonMenuSteam\steam_ca.paa"; + class TextPos + { + left = 0.0325; + top = (GUI_GRID_H - GUI_TEXT_SIZE_MEDIUM) / 2; + right = 0.005; + bottom = 0; + }; + class ShortcutPos + { + left = 0.005; + top = 0.005; + w = 0.0225; + h = 0.03; + }; +}; +class RscMapControl +{ + deletable = 0; + fade = 0; + access = 0; + type = CT_MAP_MAIN; + idc = 51; + style = ST_MULTI + ST_TITLE_BAR; + colorBackground[] = {0.929412,0.929412,0.929412,1}; + colorOutside[] = {0.929412,0.929412,0.929412,1}; + colorText[] = {0,0,0,1}; + font = "TahomaB"; + sizeEx = 0.04; + colorSea[] = {0.467,0.631,0.851,0.5}; + colorForest[] = {0.6,0.8,0.2,0.25}; + colorRocks[] = {0.5,0.5,0.5,0.5}; + colorCountlines[] = {0.647059,0.533333,0.286275,1}; + colorMainCountlines[] = {0.858824,0,0,1}; + colorCountlinesWater[] = {0.491,0.577,0.702,0.3}; + colorMainCountlinesWater[] = {0.491,0.577,0.702,0.6}; + colorForestBorder[] = {0,0,0,0}; + colorRocksBorder[] = {0,0,0,0}; + colorPowerLines[] = {0.1,0.1,0.1,1}; + colorRailWay[] = {0.8,0.2,0,1}; + colorNames[] = {0.1,0.1,0.1,0.9}; + colorInactive[] = {1,1,1,0.5}; + colorLevels[] = {0,0,0,1}; + colorTracks[] = {0.2,0.13,0,1}; + colorRoads[] = {0.2,0.13,0,1}; + colorMainRoads[] = {0,0,0,1}; + colorTracksFill[] = {1,0.88,0.65,0.3}; + colorRoadsFill[] = {1,0.88,0.65,1}; + colorMainRoadsFill[] = {0.94,0.69,0.2,1}; + colorGrid[] = {0.05,0.1,0,0.6}; + colorGridMap[] = {0.05,0.1,0,0.4}; + stickX[] = {0.2,["Gamma",1,1.5]}; + stickY[] = {0.2,["Gamma",1,1.5]}; + class Legend + { + colorBackground[] = {1,1,1,0.5}; + color[] = {0,0,0,1}; + x = SafeZoneX + GUI_GRID_W; + y = SafeZoneY + safezoneH - 4.5 * GUI_GRID_H; + w = 10 * GUI_GRID_W; + h = 3.5 * GUI_GRID_H; + font = "RobotoCondensed"; + sizeEx = GUI_TEXT_SIZE_SMALL; + }; + class ActiveMarker + { + color[] = {0.3,0.1,0.9,1}; + size = 50; + }; + class Command + { + color[] = {1,1,1,1}; + icon = "\a3\ui_f\data\map\mapcontrol\waypoint_ca.paa"; + size = 18; + importance = 1; + coefMin = 1; + coefMax = 1; + }; + class Task + { + taskNone = "#(argb,8,8,3)color(0,0,0,0)"; + taskCreated = "#(argb,8,8,3)color(0,0,0,1)"; + taskAssigned = "#(argb,8,8,3)color(1,1,1,1)"; + taskSucceeded = "#(argb,8,8,3)color(0,1,0,1)"; + taskFailed = "#(argb,8,8,3)color(1,0,0,1)"; + taskCanceled = "#(argb,8,8,3)color(1,0.5,0,1)"; + colorCreated[] = {1,1,1,1}; + colorCanceled[] = {0.7,0.7,0.7,1}; + colorDone[] = {0.7,1,0.3,1}; + colorFailed[] = {1,0.3,0.2,1}; + color[] = + { + "(profilenamespace getvariable ['IGUI_TEXT_RGB_R',0])", + "(profilenamespace getvariable ['IGUI_TEXT_RGB_G',1])", + "(profilenamespace getvariable ['IGUI_TEXT_RGB_B',1])", + "(profilenamespace getvariable ['IGUI_TEXT_RGB_A',0.8])" + }; + icon = "\A3\ui_f\data\map\mapcontrol\taskIcon_CA.paa"; + iconCreated = "\A3\ui_f\data\map\mapcontrol\taskIconCreated_CA.paa"; + iconCanceled = "\A3\ui_f\data\map\mapcontrol\taskIconCanceled_CA.paa"; + iconDone = "\A3\ui_f\data\map\mapcontrol\taskIconDone_CA.paa"; + iconFailed = "\A3\ui_f\data\map\mapcontrol\taskIconFailed_CA.paa"; + size = 27; + importance = 1; + coefMin = 1; + coefMax = 1; + }; + class CustomMark + { + color[] = {1,1,1,1}; + icon = "\a3\ui_f\data\map\mapcontrol\custommark_ca.paa"; + size = 18; + importance = 1; + coefMin = 1; + coefMax = 1; + }; + class Tree + { + color[] = {0.45,0.64,0.33,0.4}; + icon = "\A3\ui_f\data\map\mapcontrol\bush_ca.paa"; + size = 12; + importance = "0.9 * 16 * 0.05"; + coefMin = 0.25; + coefMax = 4; + }; + class SmallTree + { + color[] = {0.45,0.64,0.33,0.4}; + icon = "\A3\ui_f\data\map\mapcontrol\bush_ca.paa"; + size = 12; + importance = "0.6 * 12 * 0.05"; + coefMin = 0.25; + coefMax = 4; + }; + class Bush + { + color[] = {0.45,0.64,0.33,0.4}; + icon = "\A3\ui_f\data\map\mapcontrol\bush_ca.paa"; + size = "14/2"; + importance = "0.2 * 14 * 0.05 * 0.05"; + coefMin = 0.25; + coefMax = 4; + }; + class Church + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\church_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Chapel + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\Chapel_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Cross + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\Cross_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Rock + { + color[] = {0.1,0.1,0.1,0.8}; + icon = "\A3\ui_f\data\map\mapcontrol\rock_ca.paa"; + size = 12; + importance = "0.5 * 12 * 0.05"; + coefMin = 0.25; + coefMax = 4; + }; + class Bunker + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\bunker_ca.paa"; + size = 14; + importance = "1.5 * 14 * 0.05"; + coefMin = 0.25; + coefMax = 4; + }; + class Fortress + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\bunker_ca.paa"; + size = 16; + importance = "2 * 16 * 0.05"; + coefMin = 0.25; + coefMax = 4; + }; + class Fountain + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\fountain_ca.paa"; + size = 11; + importance = "1 * 12 * 0.05"; + coefMin = 0.25; + coefMax = 4; + }; + class ViewTower + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\viewtower_ca.paa"; + size = 16; + importance = "2.5 * 16 * 0.05"; + coefMin = 0.5; + coefMax = 4; + }; + class Lighthouse + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\lighthouse_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Quay + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\quay_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Fuelstation + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\fuelstation_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Hospital + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\hospital_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class BusStop + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\busstop_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class LineMarker + { + textureComboBoxColor = "#(argb,8,8,3)color(1,1,1,1)"; + lineWidthThin = 0.008; + lineWidthThick = 0.014; + lineDistanceMin = 3e-05; + lineLengthMin = 5; + }; + class Transmitter + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\transmitter_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Stack + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\stack_ca.paa"; + size = 16; + importance = "2 * 16 * 0.05"; + coefMin = 0.4; + coefMax = 2; + }; + class Ruin + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\ruin_ca.paa"; + size = 16; + importance = "1.2 * 16 * 0.05"; + coefMin = 1; + coefMax = 4; + }; + class Tourism + { + color[] = {0,0,0,1}; + icon = "\A3\ui_f\data\map\mapcontrol\tourism_ca.paa"; + size = 16; + importance = "1 * 16 * 0.05"; + coefMin = 0.7; + coefMax = 4; + }; + class Watertower + { + color[] = {1,1,1,1}; + icon = "\A3\ui_f\data\map\mapcontrol\watertower_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + }; + class Waypoint + { + color[] = {1,1,1,1}; + importance = 1; + coefMin = 1; + coefMax = 1; + icon = "\a3\ui_f\data\map\mapcontrol\waypoint_ca.paa"; + size = 18; + }; + class WaypointCompleted + { + color[] = {1,1,1,1}; + importance = 1; + coefMin = 1; + coefMax = 1; + icon = "\a3\ui_f\data\map\mapcontrol\waypointcompleted_ca.paa"; + size = 18; + }; + moveOnEdges = 1; + x = "SafeZoneXAbs"; + y = SafeZoneY + 1.5 * GUI_GRID_H; + w = "SafeZoneWAbs"; + h = SafeZoneH - 1.5 * GUI_GRID_H; + shadow = 0; + ptsPerSquareSea = 5; + ptsPerSquareTxt = 20; + ptsPerSquareCLn = 10; + ptsPerSquareExp = 10; + ptsPerSquareCost = 10; + ptsPerSquareFor = 9; + ptsPerSquareForEdge = 9; + ptsPerSquareRoad = 6; + ptsPerSquareObj = 9; + scaleMin = 0.001; + scaleMax = 1; + scaleDefault = 0.16; + alphaFadeStartScale = 2; + alphaFadeEndScale = 2; + colorTrails[] = {0.84,0.76,0.65,0.15}; + colorTrailsFill[] = {0.84,0.76,0.65,0.65}; + widthRailWay = 4; + fontLabel = "RobotoCondensed"; + sizeExLabel = GUI_TEXT_SIZE_SMALL; + fontGrid = "TahomaB"; + fontUnits = "TahomaB"; + sizeExUnits = GUI_TEXT_SIZE_SMALL; + fontNames = "RobotoCondensed"; + sizeExNames = GUI_TEXT_SIZE_SMALL * 2; + fontInfo = "RobotoCondensed"; + sizeExInfo = GUI_TEXT_SIZE_SMALL; + fontLevel = "TahomaB"; + text = "#(argb,8,8,3)color(1,1,1,1)"; + idcMarkerColor = -1; + idcMarkerIcon = -1; + textureComboBoxColor = "#(argb,8,8,3)color(1,1,1,1)"; + showMarkers = 1; + class power + { + icon = "\A3\ui_f\data\map\mapcontrol\power_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + color[] = {1,1,1,1}; + }; + class powersolar + { + icon = "\A3\ui_f\data\map\mapcontrol\powersolar_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + color[] = {1,1,1,1}; + }; + class powerwave + { + icon = "\A3\ui_f\data\map\mapcontrol\powerwave_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + color[] = {1,1,1,1}; + }; + class powerwind + { + icon = "\A3\ui_f\data\map\mapcontrol\powerwind_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + color[] = {1,1,1,1}; + }; + class Shipwreck + { + icon = "\A3\ui_f\data\map\mapcontrol\Shipwreck_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + color[] = {0,0,0,1}; + }; + class mosque_small + { + icon = "\lxws\data_f_lxws\img\markers\Map\musalla_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + color[] = {0,0,0,1}; + }; + class mosque + { + icon = "\lxws\data_f_lxws\img\markers\Map\mosque_CA.paa"; + size = 24; + importance = 1; + coefMin = 0.85; + coefMax = 1; + color[] = {1,1,1,1}; + }; + maxSatelliteAlpha = 0.5; + sizeExLevel = 0.03; + showCountourInterval = 1; + sizeExGrid = 0.032; +}; +class RscMapControlEmpty: RscMapControl +{ + type = CT_MAP_MAIN; + ptsPerSquareSea = 1000; + ptsPerSquareTxt = 1000; + ptsPerSquareCLn = 1000; + ptsPerSquareExp = 1000; + ptsPerSquareCost = 1000; + ptsPerSquareFor = 1000; + ptsPerSquareForEdge = 1000; + ptsPerSquareRoad = 1000; + ptsPerSquareObj = 1000; + alphaFadeStartScale = 0; + alphaFadeEndScale = 0; + colorBackground[] = {1,1,1,1}; + colorOutside[] = {1,1,1,1}; + colorSea[] = {0,0,0,0}; + colorForest[] = {0,0,0,0}; + colorForestBorder[] = {0,0,0,0}; + colorRocks[] = {0,0,0,0}; + colorRocksBorder[] = {0,0,0,0}; + colorLevels[] = {0,0,0,0}; + colorMainCountlines[] = {0,0,0,0}; + colorCountlines[] = {0,0,0,0}; + colorMainCountlinesWater[] = {0,0,0,0}; + colorCountlinesWater[] = {0,0,0,0}; + colorPowerLines[] = {0,0,0,0}; + colorRailWay[] = {0,0,0,0}; + colorNames[] = {0,0,0,0}; + colorInactive[] = {0,0,0,0}; + colorGrid[] = {0,0,0,0}; + colorGridMap[] = {0,0,0,0}; + class Task: Task + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + iconCreated = "#(argb,8,8,3)color(0,0,0,0)"; + iconCanceled = "#(argb,8,8,3)color(0,0,0,0)"; + iconDone = "#(argb,8,8,3)color(0,0,0,0)"; + iconFailed = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + colorCreated[] = {0,0,0,0}; + colorCanceled[] = {0,0,0,0}; + colorDone[] = {0,0,0,0}; + colorFailed[] = {0,0,0,0}; + size = 0; + }; + class Waypoint: Waypoint + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class WaypointCompleted: WaypointCompleted + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class CustomMark: CustomMark + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Command: Command + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Bush: Bush + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Rock: Rock + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class SmallTree: SmallTree + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Tree: Tree + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class busstop: BusStop + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class fuelstation: Fuelstation + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class hospital: Hospital + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class church: Church + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class lighthouse: Lighthouse + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class power: power + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class powersolar: powersolar + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class powerwave: powerwave + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class powerwind: powerwind + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class quay: Quay + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class shipwreck: Shipwreck + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class transmitter: Transmitter + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class watertower: Watertower + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Bunker: Bunker + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Cross: Cross + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Fortress: Fortress + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Fountain: Fountain + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Chapel: Chapel + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Ruin: Ruin + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Stack: Stack + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class Tourism: Tourism + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; + class ViewTower: ViewTower + { + icon = "#(argb,8,8,3)color(0,0,0,0)"; + color[] = {0,0,0,0}; + size = 0; + }; +}; +class RscCheckBox +{ + idc = -1; + type = CT_CHECKBOX; + deletable = 0; + style = ST_LEFT; + checked = 0; + x = "0.375 * safezoneW + safezoneX"; + y = "0.36 * safezoneH + safezoneY"; + w = "0.025 * safezoneW"; + h = "0.04 * safezoneH"; + color[] = {1,1,1,0.7}; + colorFocused[] = {1,1,1,1}; + colorHover[] = {1,1,1,1}; + colorPressed[] = {1,1,1,1}; + colorDisabled[] = {1,1,1,0.2}; + colorBackground[] = {0,0,0,0}; + colorBackgroundFocused[] = {0,0,0,0}; + colorBackgroundHover[] = {0,0,0,0}; + colorBackgroundPressed[] = {0,0,0,0}; + colorBackgroundDisabled[] = {0,0,0,0}; + textureChecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_checked_ca.paa"; + textureUnchecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_unchecked_ca.paa"; + textureFocusedChecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_checked_ca.paa"; + textureFocusedUnchecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_unchecked_ca.paa"; + textureHoverChecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_checked_ca.paa"; + textureHoverUnchecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_unchecked_ca.paa"; + texturePressedChecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_checked_ca.paa"; + texturePressedUnchecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_unchecked_ca.paa"; + textureDisabledChecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_checked_ca.paa"; + textureDisabledUnchecked = "A3\Ui_f\data\GUI\RscCommon\RscCheckBox\CheckBox_unchecked_ca.paa"; + tooltipColorText[] = {1,1,1,1}; + tooltipColorBox[] = {1,1,1,1}; + tooltipColorShade[] = {0,0,0,0.65}; + soundEnter[] = + { + "", + 0.1, + 1 + }; + soundPush[] = + { + "", + 0.1, + 1 + }; + soundClick[] = + { + "", + 0.1, + 1 + }; + soundEscape[] = + { + "", + 0.1, + 1 + }; +}; diff --git a/arma/ui.7z b/arma/ui.7z new file mode 100644 index 0000000..dd6a9c3 Binary files /dev/null and b/arma/ui.7z differ diff --git a/arma/ui/apps/base.css b/arma/ui/apps/base.css deleted file mode 100644 index a05b977..0000000 --- a/arma/ui/apps/base.css +++ /dev/null @@ -1,175 +0,0 @@ -:root { - --bg-app: #fdfcf8; - --bg-surface: #ffffff; - --bg-surface-hover: #f1f5f9; - --primary: #475569; - --primary-hover: #1e293b; - --text-main: #1f2937; - --text-muted: #64748b; - --text-inverse: #f8fafc; - --border: #e2e8f0; - --radius: 8px; - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --footer-bg: #1e293b; -} - -body { - font-family: - "Inter", - system-ui, - -apple-system, - sans-serif; - margin: 0; - padding: 0; - background: var(--bg-app); - color: var(--text-main); - line-height: 1.6; -} - -#app { - min-height: 100vh; -} - -main { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.container { - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 2rem; - flex: 1; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -.header { - text-align: center; - margin-bottom: 3rem; - padding-bottom: 2rem; - border-bottom: 1px solid var(--border); - - h1 { - font-size: 2.5rem; - font-weight: 700; - margin-bottom: 0.5rem; - letter-spacing: -0.025em; - color: var(--primary-hover); - } - - p { - color: var(--text-muted); - font-size: 1.1rem; - } -} - -.card { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 2rem; - box-shadow: var(--shadow); - text-align: center; - - h2 { - margin-top: 0; - font-size: 1.8rem; - color: var(--primary-hover); - } -} - -button { - background: var(--primary); - color: white; - border: none; - padding: 0.75rem 1.5rem; - border-radius: var(--radius); - cursor: pointer; - font-size: 1rem; - font-weight: 500; - transition: all 0.2s ease; - - &:hover { - background: var(--primary-hover); - transform: translateY(-1px); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); - } - - &:disabled { - cursor: not-allowed; - opacity: 0.65; - transform: none; - box-shadow: none; - } - - & + & { - margin-left: 1rem; - } -} - -.footer { - margin-top: auto; - background: var(--footer-bg); - color: var(--text-inverse); - display: block; - - .wrapper { - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 3rem 2rem; - box-sizing: border-box; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4rem; - } - - h3 { - color: var(--text-inverse); - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.1em; - font-weight: 700; - margin-bottom: 1.5rem; - border-bottom: 1px solid #475569; - padding-bottom: 0.5rem; - margin-right: 1rem; - } - - ul { - li { - color: #cbd5e1; - font-size: 0.95rem; - margin-bottom: 0.75rem; - cursor: pointer; - transition: color 0.2s; - - &:hover { - color: white; - } - } - } -} - -@media (max-width: 960px) { - .container { - padding: 1.5rem; - } - - .header { - margin-bottom: 2rem; - padding-bottom: 1.5rem; - - h1 { - font-size: 2rem; - } - } - - .footer .wrapper { - grid-template-columns: 1fr; - } -} diff --git a/arma/ui/apps/components/AppShell.js b/arma/ui/apps/components/AppShell.js deleted file mode 100644 index aa56bfc..0000000 --- a/arma/ui/apps/components/AppShell.js +++ /dev/null @@ -1,107 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - const store = RegistryApp.store; - - RegistryApp.components = RegistryApp.components || {}; - - RegistryApp.components.App = function App() { - const Navbar = window.SharedUI.componentFns.Navbar; - const Header = window.SharedUI.componentFns.Header; - const Footer = window.SharedUI.componentFns.Footer; - const HomeView = RegistryApp.componentFns.HomeView; - const RegistrationView = RegistryApp.componentFns.RegistrationView; - const PortalApp = - window.OrgPortal && window.OrgPortal.components - ? window.OrgPortal.components.App - : null; - - const view = store.getView(); - const viewLabel = - view === "create" - ? "Organization Registration" - : view === "portal" - ? "Organization Portal" - : "Entry Hub"; - const actionLabel = view === "portal" ? "Sign Out" : "Close"; - const footerSections = [ - { - title: "Registry Resources", - items: [ - "Registration Guidelines", - "Tax & Fee Schedule", - "Legal Compliance", - "Trademark Database", - ], - }, - { - title: "Bureau Support", - items: [ - "Office: Sector 7 Admin Block", - "Hours: 0800 - 1600 (GST)", - "Helpdesk: 555-01-REGISTRY", - "support@org-bureau.gov", - ], - }, - ]; - - function closeRegistry() { - if ( - typeof A3API !== "undefined" && - typeof A3API.SendAlert === "function" - ) { - A3API.SendAlert( - JSON.stringify({ - event: "org::close", - data: {}, - }), - ); - return; - } - - store.setView("home"); - } - - if (view === "portal" && PortalApp) { - return h( - "div", - null, - Navbar({ - title: "Global Organization Network", - viewLabel, - actionLabel, - onAction: closeRegistry, - }), - PortalApp(), - ); - } - - let mainContent; - if (view === "home") { - mainContent = HomeView(); - } else if (view === "create") { - mainContent = RegistrationView(); - } - - return h( - "main", - null, - Navbar({ - title: "Global Organization Network", - viewLabel, - actionLabel, - onAction: closeRegistry, - }), - h( - "div", - { className: "container" }, - Header({ - title: "Global Organization Network", - onTitleClick: () => store.setView("home"), - }), - mainContent, - ), - Footer({ sections: footerSections }), - ); - }; -})(); diff --git a/arma/ui/apps/components/footer.js b/arma/ui/apps/components/footer.js deleted file mode 100644 index 4401f4b..0000000 --- a/arma/ui/apps/components/footer.js +++ /dev/null @@ -1,32 +0,0 @@ -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Footer = function Footer({ sections = [] }) { - return h( - "div", - { className: "footer" }, - h( - "div", - { className: "wrapper" }, - ...sections.map((section) => - h( - "div", - null, - h("h3", null, section.title), - h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - ...(section.items || []).map((item) => - h("li", null, item), - ), - ), - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/components/header.js b/arma/ui/apps/components/header.js deleted file mode 100644 index 6734ac1..0000000 --- a/arma/ui/apps/components/header.js +++ /dev/null @@ -1,27 +0,0 @@ -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Header = function Header({ - title, - subtitle = "Organization Registration & Management Portal", - onTitleClick = null, - }) { - return h( - "div", - { className: "header" }, - h( - "h1", - { - style: { cursor: onTitleClick ? "pointer" : "default" }, - onClick: onTitleClick, - }, - title, - ), - h("p", null, subtitle), - ); - }; -})(); diff --git a/arma/ui/apps/components/hero.js b/arma/ui/apps/components/hero.js deleted file mode 100644 index a022e70..0000000 --- a/arma/ui/apps/components/hero.js +++ /dev/null @@ -1,35 +0,0 @@ -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Hero = function Hero({ - className = "", - kicker = "", - title = "", - subtitle = "", - meta = "", - }) { - const finalClassName = [ - "card org-panel org-span-12 org-page-header", - className, - ] - .filter(Boolean) - .join(" "); - - return h( - "section", - { className: finalClassName }, - h( - "div", - { className: "org-page-heading" }, - h("span", { className: "org-page-kicker" }, kicker), - h("h1", { className: "org-page-title" }, title), - h("p", { className: "org-page-subtitle" }, subtitle), - h("span", { className: "org-page-meta" }, meta), - ), - ); - }; -})(); diff --git a/arma/ui/apps/components/modal.js b/arma/ui/apps/components/modal.js deleted file mode 100644 index 862b115..0000000 --- a/arma/ui/apps/components/modal.js +++ /dev/null @@ -1,190 +0,0 @@ -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const scopeAttr = "data-ui-modal"; - const scopeSelector = `[${scopeAttr}]`; - const modalCss = ` -${scopeSelector} { - position: fixed; - inset: 0; - background: rgb(15 23 42 / 0.38); - display: flex; - align-items: center; - justify-content: center; - padding: 1.5rem; - z-index: 20; -} - -${scopeSelector} .app-modal-card { - width: min(100%, 30rem); - margin-bottom: 0; - text-align: left; -} - -${scopeSelector} .app-modal-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1rem; -} - -${scopeSelector} .app-modal-title { - margin: 0; - color: var(--primary-hover); - font-size: 1.45rem; -} - -${scopeSelector} .app-modal-close { - width: 2.25rem; - height: 2.25rem; - padding: 0; - background: var(--bg-surface); - color: var(--text-main); - border: 1px solid var(--border); - box-shadow: none; - transform: none; -} - -${scopeSelector} .app-modal-close:hover { - background: var(--bg-surface-hover); - color: var(--text-main); - box-shadow: none; - transform: none; -} - -${scopeSelector} .app-modal-form { - display: flex; - flex-direction: column; - gap: 1rem; -} - -${scopeSelector} .app-modal-form label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; -} - -${scopeSelector} .app-modal-form input, -${scopeSelector} .app-modal-form select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: border-color 0.2s, box-shadow 0.2s; -} - -${scopeSelector} .app-modal-form input:focus, -${scopeSelector} .app-modal-form select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12); -} - -${scopeSelector} .app-modal-form input:disabled, -${scopeSelector} .app-modal-form select:disabled { - background: #f1f5f9; - color: var(--text-muted); - cursor: not-allowed; -} - -${scopeSelector} .app-modal-actions { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 0.75rem; - margin-top: 0.5rem; -} - -${scopeSelector} .app-modal-actions button + button, -${scopeSelector} .app-modal-danger-actions button + button { - margin-left: 0; -} - -${scopeSelector} .app-modal-danger { - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid #fecaca; - border-radius: var(--radius); - background: #fff1f2; - align-items: flex-start; -} - -${scopeSelector} .app-modal-danger p { - margin: 0; - color: var(--text-main); -} - -${scopeSelector} .app-modal-danger-actions { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .app-modal-head, - ${scopeSelector} .app-modal-danger { - flex-direction: column; - align-items: flex-start; - } -} -`; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Modal = function Modal({ - title = "", - body = null, - onClose = null, - }) { - ensureScopedStyle("shared-modal", modalCss); - - return h( - "div", - { - className: "app-modal-backdrop", - [scopeAttr]: "", - onClick: (e) => { - if (e.target === e.currentTarget && onClose) { - onClose(); - } - }, - }, - h( - "div", - { className: "card app-modal-card" }, - h( - "div", - { className: "app-modal-head" }, - h( - "div", - null, - h("h2", { className: "app-modal-title" }, title), - ), - h( - "button", - { - type: "button", - className: "app-modal-close", - onClick: onClose, - "aria-label": "Close dialog", - }, - "x", - ), - ), - body, - ), - ); - }; -})(); diff --git a/arma/ui/apps/components/navbar.js b/arma/ui/apps/components/navbar.js deleted file mode 100644 index 2b096bb..0000000 --- a/arma/ui/apps/components/navbar.js +++ /dev/null @@ -1,129 +0,0 @@ -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const scopeAttr = "data-ui-navbar"; - const scopeSelector = `[${scopeAttr}]`; - const navbarCss = ` -${scopeSelector} { - background: var(--bg-surface); - border-bottom: 1px solid var(--border); - box-shadow: var(--shadow); -} - -${scopeSelector} .app-navbar-inner { - display: flex; - justify-content: space-between; - align-items: center; - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 1rem 2rem; - box-sizing: border-box; -} - -${scopeSelector} .app-navbar-brand { - display: flex; - flex-direction: column; - gap: 0.125rem; -} - -${scopeSelector} .app-navbar-kicker { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); - font-weight: 600; -} - -${scopeSelector} .app-navbar-title { - font-size: 1.25rem; - font-weight: 700; - color: var(--primary-hover); - letter-spacing: -0.025em; -} - -${scopeSelector} .app-navbar-actions { - display: flex; - align-items: center; - gap: 1.5rem; -} - -${scopeSelector} .app-navbar-view { - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - font-weight: 600; -} - -${scopeSelector} .app-close-btn { - background: transparent; - color: var(--text-muted); - border: 1px solid var(--border); - padding: 0.5rem 1rem; - font-size: 0.85rem; -} - -${scopeSelector} .app-close-btn:hover { - background: var(--bg-surface-hover); - color: var(--primary-hover); - border-color: var(--primary); - transform: none; - box-shadow: none; -} - -@media (max-width: 960px) { - ${scopeSelector} .app-navbar-inner { - flex-direction: column; - align-items: flex-start; - padding: 1rem 1.5rem; - } - - ${scopeSelector} .app-navbar-actions { - align-items: flex-start; - } -} -`; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Navbar = function Navbar({ - kicker = "ORBIS", - title = "", - viewLabel = "", - actionLabel = "", - onAction = null, - }) { - ensureScopedStyle("shared-navbar", navbarCss); - - return h( - "nav", - { className: "app-navbar", [scopeAttr]: "" }, - h( - "div", - { className: "app-navbar-inner" }, - h( - "div", - { className: "app-navbar-brand" }, - h("span", { className: "app-navbar-kicker" }, kicker), - h("span", { className: "app-navbar-title" }, title), - ), - h( - "div", - { className: "app-navbar-actions" }, - h("span", { className: "app-navbar-view" }, viewLabel), - h( - "button", - { - type: "button", - className: "app-close-btn", - onClick: onAction, - }, - actionLabel, - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/components/panelCard.js b/arma/ui/apps/components/panelCard.js deleted file mode 100644 index b927c98..0000000 --- a/arma/ui/apps/components/panelCard.js +++ /dev/null @@ -1,83 +0,0 @@ -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const scopeAttr = "data-ui-panel-card"; - const scopeSelector = `[${scopeAttr}]`; - const panelCardCss = ` -${scopeSelector} .org-panel-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1.5rem; -} - -${scopeSelector} .org-eyebrow { - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--text-muted); - margin-bottom: 0.4rem; -} - -${scopeSelector} .org-panel-title { - margin: 0; - color: var(--primary-hover); - font-size: 1.45rem; -} - -${scopeSelector} .org-panel-subtitle { - margin: 0.35rem 0 0; - color: var(--text-muted); - font-size: 0.95rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-panel-head { - flex-direction: column; - align-items: flex-start; - } -} -`; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.PanelCard = function PanelCard({ - className = "", - eyebrow = "", - title = "", - subtitle = "", - headerExtras = null, - body = null, - rootProps = {}, - }) { - const finalClassName = ["card org-panel", className] - .filter(Boolean) - .join(" "); - ensureScopedStyle("shared-panel-card", panelCardCss); - - return h( - "section", - { className: finalClassName, [scopeAttr]: "", ...rootProps }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - eyebrow - ? h("div", { className: "org-eyebrow" }, eyebrow) - : null, - h("h2", { className: "org-panel-title" }, title), - subtitle - ? h("p", { className: "org-panel-subtitle" }, subtitle) - : null, - ), - headerExtras, - ), - body, - ); - }; -})(); diff --git a/arma/ui/apps/components/portal/activityCard.js b/arma/ui/apps/components/portal/activityCard.js deleted file mode 100644 index 6060f41..0000000 --- a/arma/ui/apps/components/portal/activityCard.js +++ /dev/null @@ -1,79 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const scopeAttr = "data-ui-activity-card"; - const scopeSelector = `[${scopeAttr}]`; - const activityCardCss = ` -${scopeSelector} .org-activity-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-activity-row { - padding: 1rem; - border: 1px solid var(--border); - border-left: 3px solid #94a3b8; - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-activity-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); - border-left-color: #64748b; -} - -${scopeSelector} .org-activity-row p { - margin: 0; - color: var(--text-main); -} - -${scopeSelector} .org-activity-time { - display: inline-block; - margin-bottom: 0.35rem; - color: var(--text-muted); - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.05em; - text-transform: uppercase; -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.ActivityCard = function ActivityCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - ensureScopedStyle("portal-activity-card", activityCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-6", - title: "Command Feed", - subtitle: "Recent organization-level actions and updates.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-activity-list" }, - ...portalData.activity.map((item) => - h( - "article", - { className: "org-activity-row" }, - h( - "span", - { className: "org-activity-time" }, - item.time, - ), - h("p", null, item.text), - ), - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/assetsCard.js b/arma/ui/apps/components/portal/assetsCard.js deleted file mode 100644 index 8b65094..0000000 --- a/arma/ui/apps/components/portal/assetsCard.js +++ /dev/null @@ -1,94 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-assets-card"; - const scopeSelector = `[${scopeAttr}]`; - const assetsCardCss = ` -${scopeSelector} .org-simple-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-simple-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-simple-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-simple-name { - color: var(--primary-hover); -} - -${scopeSelector} .org-simple-meta { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 1rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-simple-row { - flex-direction: column; - align-items: flex-start; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.AssetsCard = function AssetsCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const SimpleStat = OrgPortal.componentFns.SimpleStat; - ensureScopedStyle("portal-assets-card", assetsCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-7", - title: "Assets", - subtitle: "Inventory supplies and equipment with quantity totals.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-simple-list" }, - ...portalData.assets.map((asset) => - h( - "article", - { className: "org-simple-row" }, - h( - "strong", - { className: "org-simple-name" }, - asset.name, - ), - h( - "div", - { className: "org-simple-meta" }, - SimpleStat( - "Type", - actions.formatAssetType(asset.type), - ), - SimpleStat("Quantity", asset.quantity), - ), - ), - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/dangerCard.js b/arma/ui/apps/components/portal/dangerCard.js deleted file mode 100644 index 4c825c3..0000000 --- a/arma/ui/apps/components/portal/dangerCard.js +++ /dev/null @@ -1,70 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const permissions = OrgPortal.permissions; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-danger-card"; - const scopeSelector = `[${scopeAttr}]`; - const dangerCardCss = ` -${scopeSelector} { - border-color: #fecaca; - background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%); -} - -${scopeSelector} .org-danger-copy { - margin-bottom: 1rem; -} - -${scopeSelector} .org-danger-copy strong, -${scopeSelector} .org-danger-copy p { - display: block; -} - -${scopeSelector} .org-danger-copy p { - margin: 0.4rem 0 0; - color: var(--text-muted); -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.DangerCard = function DangerCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - ensureScopedStyle("portal-danger-card", dangerCardCss); - - if (!permissions.canDisbandOrg()) { - return null; - } - - return PanelCard({ - className: "org-span-12 org-danger-panel", - title: "Organization Controls", - subtitle: - "Leader-only actions for membership and permanent organization removal.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - null, - h( - "div", - { className: "org-danger-copy" }, - h("strong", null, "Disband organization"), - h( - "p", - null, - "This removes the organization and revokes access to the portal for all members.", - ), - ), - h( - "button", - { - type: "button", - className: "org-danger-btn", - onClick: () => actions.openModal("disband"), - }, - "Disband Organization", - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/fleetCard.js b/arma/ui/apps/components/portal/fleetCard.js deleted file mode 100644 index 6869901..0000000 --- a/arma/ui/apps/components/portal/fleetCard.js +++ /dev/null @@ -1,101 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-fleet-card"; - const scopeSelector = `[${scopeAttr}]`; - const fleetCardCss = ` -${scopeSelector} .org-simple-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} { - min-height: 32.5rem; - max-height: 32.5rem; -} - -${scopeSelector} .org-simple-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-simple-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-simple-name { - color: var(--primary-hover); -} - -${scopeSelector} .org-simple-meta { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 1rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-simple-row { - flex-direction: column; - align-items: flex-start; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.FleetCard = function FleetCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const SimpleStat = OrgPortal.componentFns.SimpleStat; - ensureScopedStyle("portal-fleet-card", fleetCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-7", - title: "Fleet", - subtitle: - "Individual vehicles with type, status, and overall damage.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-simple-list" }, - ...portalData.fleet.map((unit) => - h( - "article", - { className: "org-simple-row" }, - h( - "strong", - { className: "org-simple-name" }, - unit.name, - ), - h( - "div", - { className: "org-simple-meta" }, - SimpleStat( - "Type", - actions.formatVehicleType(unit.type), - ), - SimpleStat("Status", unit.status), - SimpleStat("Damage", unit.damage), - ), - ), - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/futureCard.js b/arma/ui/apps/components/portal/futureCard.js deleted file mode 100644 index 343835f..0000000 --- a/arma/ui/apps/components/portal/futureCard.js +++ /dev/null @@ -1,126 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const scopeAttr = "data-ui-future-card"; - const ROADMAP = [ - { - name: "Contracts Board", - status: "Planned", - detail: "Track payouts, assignments, and claim approvals.", - }, - { - name: "Diplomacy", - status: "Future Review", - detail: "Possible future module pending a full design and scope review.", - }, - { - name: "Logistics Queue", - status: "Future Review", - detail: "Possible future module pending a full design and scope review.", - }, - { - name: "Permissions", - status: "Future Review", - detail: "Possible future module pending a full design and scope review.", - }, - ]; - const scopeSelector = `[${scopeAttr}]`; - const futureCardCss = ` -${scopeSelector} .org-roadmap-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - flex: 1; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-roadmap-card { - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.7rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-roadmap-card:nth-child(4n + 2), -${scopeSelector} .org-roadmap-card:nth-child(4n + 3) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(100 116 139 / 0.4); -} - -${scopeSelector} .org-roadmap-card p { - margin: 0; - color: var(--text-main); -} - -${scopeSelector} .org-list-tag { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.2rem 0.55rem; - border-radius: 999px; - font-size: 0.72rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - background: #e2e8f0; - color: var(--primary-hover); -} - -${scopeSelector} .org-roadmap-card:nth-child(4n + 2) .org-list-tag, -${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag { - background: #cbd5e1; - color: #1e293b; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-roadmap-grid { - grid-template-columns: 1fr; - } - - ${scopeSelector} .org-roadmap-card:nth-child(4n + 3) { - background: #f8fafc; - border-color: var(--border); - } - - ${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag { - background: #e2e8f0; - color: var(--primary-hover); - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.FutureCard = function FutureCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - ensureScopedStyle("portal-future-card", futureCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-6", - title: "Expansion Slots", - subtitle: - "Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-roadmap-grid" }, - ...ROADMAP.map((item) => - h( - "article", - { className: "org-roadmap-card" }, - h("span", { className: "org-list-tag" }, item.status), - h("strong", null, item.name), - h("p", null, item.detail), - ), - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/membersCard.js b/arma/ui/apps/components/portal/membersCard.js deleted file mode 100644 index 3bc616e..0000000 --- a/arma/ui/apps/components/portal/membersCard.js +++ /dev/null @@ -1,116 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const store = OrgPortal.store; - const permissions = OrgPortal.permissions; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-members-card"; - const scopeSelector = `[${scopeAttr}]`; - const membersCardCss = ` -${scopeSelector} .org-name-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-name-row { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-name-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-name-row button { - margin-left: auto; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-name-row { - flex-direction: column; - align-items: flex-start; - } - - ${scopeSelector} .org-name-row button { - margin-left: 0; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.MembersCard = function MembersCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const members = store.getMembers(); - const allowMemberManagement = permissions.canManageMembers(); - ensureScopedStyle("portal-members-card", membersCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-5", - title: "Members", - subtitle: - "Current roster listing. The organization owner cannot be removed.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-name-list" }, - ...members.map((member) => { - const canRemoveMember = - allowMemberManagement && - !actions.isOwnerMember(member.name); - - return h( - "article", - { className: "org-name-row" }, - h("strong", null, member.name), - canRemoveMember - ? h( - "button", - { - type: "button", - className: "org-danger-btn org-icon-btn", - title: `Remove ${member.name}`, - "aria-label": `Remove ${member.name}`, - onClick: () => - actions.removeMember(member.name), - }, - h( - "svg", - { - className: "org-icon", - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "aria-hidden": "true", - }, - h("path", { d: "M9 3h6" }), - h("path", { d: "M4 7h16" }), - h("path", { d: "M6 7l1 13h10l1-13" }), - h("path", { d: "M10 11v6" }), - h("path", { d: "M14 11v6" }), - ), - ) - : null, - ); - }), - ), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/metricCard.js b/arma/ui/apps/components/portal/metricCard.js deleted file mode 100644 index ed0d7a3..0000000 --- a/arma/ui/apps/components/portal/metricCard.js +++ /dev/null @@ -1,77 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const scopeAttr = "data-ui-metric-card"; - const scopeSelector = `[${scopeAttr}]`; - const metricCardCss = ` -${scopeSelector} { - display: flex; - flex-direction: column; - gap: 0.45rem; - padding: 1rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); -} - -${scopeSelector}:nth-child(4n + 2), -${scopeSelector}:nth-child(4n + 3) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%); - border-color: rgb(100 116 139 / 0.35); - box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6); -} - -${scopeSelector} .org-metric-label { - font-size: 0.76rem; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--text-muted); -} - -${scopeSelector} .org-metric-value { - font-size: 1.8rem; - color: var(--primary-hover); - line-height: 1.1; -} - -${scopeSelector}:nth-child(4n + 2) .org-metric-value, -${scopeSelector}:nth-child(4n + 3) .org-metric-value { - color: #334155; -} - -${scopeSelector} .org-metric-note { - color: var(--text-muted); - font-size: 0.9rem; -} - -@media (max-width: 960px) { - ${scopeSelector}:nth-child(4n + 3) { - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); - border-color: var(--border); - box-shadow: none; - } - - ${scopeSelector}:nth-child(4n + 3) .org-metric-value { - color: var(--primary-hover); - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.MetricCard = function MetricCard( - label, - value, - note, - ) { - ensureScopedStyle("portal-metric-card", metricCardCss); - - return h( - "div", - { className: "org-metric-card", [scopeAttr]: "" }, - h("span", { className: "org-metric-label" }, label), - h("strong", { className: "org-metric-value" }, value), - h("span", { className: "org-metric-note" }, note), - ); - }; -})(); diff --git a/arma/ui/apps/components/portal/modalLayer.js b/arma/ui/apps/components/portal/modalLayer.js deleted file mode 100644 index a0f0d3b..0000000 --- a/arma/ui/apps/components/portal/modalLayer.js +++ /dev/null @@ -1,257 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const actions = OrgPortal.actions; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.ModalLayer = function ModalLayer() { - const Modal = window.SharedUI.componentFns.Modal; - const modal = store.getModal(); - if (!modal) { - return null; - } - - const members = store.getMembers(); - const memberSelectProps = - members.length === 0 ? { disabled: true } : {}; - - let title = ""; - let body = null; - - if (modal.type === "payroll") { - title = "Run Payroll"; - body = h( - "div", - { className: "app-modal-form" }, - h( - "div", - null, - h("label", null, "Amount Per Member"), - h("input", { - id: "treasury-payroll-amount", - type: "number", - min: "1", - placeholder: "500", - autofocus: "true", - }), - ), - h( - "div", - { className: "app-modal-actions" }, - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => actions.closeModal(), - }, - "Cancel", - ), - h( - "button", - { - type: "button", - onClick: () => { - if ( - actions.runPayroll( - actions.parseAmount( - actions.getInputValue( - "treasury-payroll-amount", - ), - ), - ) - ) { - actions.closeModal(); - } - }, - }, - "Run Payroll", - ), - ), - ); - } else if (modal.type === "transfer") { - title = "Send Funds"; - body = h( - "div", - { className: "app-modal-form" }, - h( - "div", - null, - h("label", null, "Member"), - h( - "select", - { - id: "treasury-transfer-member", - ...memberSelectProps, - }, - ...members.map((member) => - h("option", { value: member.name }, member.name), - ), - ), - ), - h( - "div", - null, - h("label", null, "Amount"), - h("input", { - id: "treasury-transfer-amount", - type: "number", - min: "1", - placeholder: "1500", - }), - ), - h( - "div", - { className: "app-modal-actions" }, - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => actions.closeModal(), - }, - "Cancel", - ), - h( - "button", - { - type: "button", - ...memberSelectProps, - onClick: () => { - if ( - actions.sendFundsToMember( - String( - actions.getInputValue( - "treasury-transfer-member", - ) || "", - ), - actions.parseAmount( - actions.getInputValue( - "treasury-transfer-amount", - ), - ), - ) - ) { - actions.closeModal(); - } - }, - }, - "Send Funds", - ), - ), - ); - } else if (modal.type === "credit") { - title = "Assign Credit Line"; - body = h( - "div", - { className: "app-modal-form" }, - h( - "div", - null, - h("label", null, "Member"), - h( - "select", - { id: "treasury-credit-member", ...memberSelectProps }, - ...members.map((member) => - h("option", { value: member.name }, member.name), - ), - ), - ), - h( - "div", - null, - h("label", null, "Credit Amount"), - h("input", { - id: "treasury-credit-amount", - type: "number", - min: "1", - placeholder: "5000", - }), - ), - h( - "div", - { className: "app-modal-actions" }, - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => actions.closeModal(), - }, - "Cancel", - ), - h( - "button", - { - type: "button", - ...memberSelectProps, - onClick: () => { - if ( - actions.grantCreditLine( - String( - actions.getInputValue( - "treasury-credit-member", - ) || "", - ), - actions.parseAmount( - actions.getInputValue( - "treasury-credit-amount", - ), - ), - ) - ) { - actions.closeModal(); - } - }, - }, - "Assign Credit Line", - ), - ), - ); - } else if (modal.type === "disband") { - title = "Disband Organization"; - body = h( - "div", - { className: "app-modal-danger" }, - h( - "p", - null, - "This action is permanent. Disband ", - portalData.org.name, - "?", - ), - h( - "div", - { className: "app-modal-danger-actions" }, - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => actions.closeModal(), - }, - "Cancel", - ), - h( - "button", - { - type: "button", - className: "org-danger-btn", - onClick: () => actions.disbandOrganization(), - }, - "Confirm Disband", - ), - ), - ); - } - - return Modal({ - title, - body, - onClose: () => actions.closeModal(), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/overviewCard.js b/arma/ui/apps/components/portal/overviewCard.js deleted file mode 100644 index 87fada3..0000000 --- a/arma/ui/apps/components/portal/overviewCard.js +++ /dev/null @@ -1,175 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-overview-card"; - const scopeSelector = `[${scopeAttr}]`; - const overviewCardCss = ` -${scopeSelector} .org-hero-grid { - display: grid; - grid-template-columns: 1.3fr 1fr; - gap: 1.5rem; - align-items: start; -} - -${scopeSelector} .org-summary { - margin: 0; - font-size: 1.05rem; - color: var(--text-main); -} - -${scopeSelector} .org-meta-row { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1rem; - margin-top: 1.5rem; -} - -${scopeSelector} .org-meta-item { - display: flex; - flex-direction: column; - gap: 0.4rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-meta-item:nth-child(even) { - background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-meta-label { - font-size: 0.76rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-meta-value { - font-size: 1rem; - font-weight: 600; - color: var(--primary-hover); -} - -${scopeSelector} .org-metric-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-hero-grid, - ${scopeSelector} .org-meta-row, - ${scopeSelector} .org-metric-grid { - grid-template-columns: 1fr; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.OverviewCard = function OverviewCard() { - const MetricCard = OrgPortal.componentFns.MetricCard; - const PanelCard = window.SharedUI.componentFns.PanelCard; - const readiness = actions.getAssetReadiness(); - const headquarters = portalData.org.headquarters || "ArmA Verse"; - ensureScopedStyle("portal-overview-card", overviewCardCss); - - return PanelCard({ - className: "org-span-12", - eyebrow: portalData.org.tag, - title: "Organization Overview", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-hero-grid" }, - h( - "div", - { className: "org-hero-copy" }, - h( - "p", - { className: "org-summary" }, - portalData.org.type, - " operating from ", - headquarters, - ". Treasury, fleet status, inventory, and roster management are surfaced here first.", - ), - h( - "div", - { className: "org-meta-row" }, - h( - "div", - { className: "org-meta-item" }, - h( - "span", - { className: "org-meta-label" }, - "Director", - ), - h( - "span", - { className: "org-meta-value" }, - actions.formatDisplayName(portalData.org.owner), - ), - ), - h( - "div", - { className: "org-meta-item" }, - h( - "span", - { className: "org-meta-label" }, - "Active Members", - ), - h( - "span", - { className: "org-meta-value" }, - `${store.getMembers().length} total`, - ), - ), - h( - "div", - { className: "org-meta-item" }, - h( - "span", - { className: "org-meta-label" }, - "Fleet Readiness", - ), - h( - "span", - { className: "org-meta-value" }, - readiness === null ? "N/A" : `${readiness}%`, - ), - ), - ), - ), - h( - "div", - { className: "org-metric-grid" }, - MetricCard( - "Org Funds", - actions.formatCurrency(store.getFunds()), - "Organization treasury balance", - ), - MetricCard( - "Reputation", - portalData.reputation, - "Organization standing", - ), - MetricCard( - "Asset Lines", - portalData.assets.length, - "Tracked supply and equipment entries", - ), - MetricCard( - "Fleet Vehicles", - portalData.fleet.length, - "Tracked air, ground, and naval vehicles", - ), - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/components/portal/simpleStat.js b/arma/ui/apps/components/portal/simpleStat.js deleted file mode 100644 index eceb6f6..0000000 --- a/arma/ui/apps/components/portal/simpleStat.js +++ /dev/null @@ -1,39 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const scopeAttr = "data-ui-simple-stat"; - const scopeSelector = `[${scopeAttr}]`; - const simpleStatCss = ` -${scopeSelector} { - display: flex; - flex-direction: column; - gap: 0.2rem; - min-width: 90px; -} - -${scopeSelector} .org-simple-label { - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-simple-value { - font-size: 0.95rem; - color: var(--text-main); -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) { - ensureScopedStyle("portal-simple-stat", simpleStatCss); - - return h( - "div", - { className: "org-simple-stat", [scopeAttr]: "" }, - h("span", { className: "org-simple-label" }, label), - h("strong", { className: "org-simple-value" }, value), - ); - }; -})(); diff --git a/arma/ui/apps/components/portal/treasuryCard.js b/arma/ui/apps/components/portal/treasuryCard.js deleted file mode 100644 index b43549b..0000000 --- a/arma/ui/apps/components/portal/treasuryCard.js +++ /dev/null @@ -1,430 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle, createSignal } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const permissions = OrgPortal.permissions; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-treasury-card"; - const scopeSelector = `[${scopeAttr}]`; - const [getTreasuryTab, setTreasuryTab] = createSignal("overview"); - const [getTreasuryMenuOpen, setTreasuryMenuOpen] = createSignal(false); - const treasuryCardCss = ` -${scopeSelector} .org-treasury-menu { - position: relative; -} - -${scopeSelector} .org-menu-btn { - width: 2.75rem; - height: 2.75rem; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - border: 1px solid var(--border); - background: #f8fafc; - color: var(--text-muted); -} - -${scopeSelector} .org-menu-btn:hover { - color: var(--primary-hover); - border-color: rgb(148 163 184 / 0.65); -} - -${scopeSelector} .org-menu-btn svg { - width: 1.1rem; - height: 1.1rem; -} - -${scopeSelector} .org-menu-dropdown { - position: absolute; - top: calc(100% + 0.6rem); - right: 0; - min-width: 10.5rem; - padding: 0.45rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #fff; - box-shadow: 0 12px 28px rgb(15 23 42 / 0.12); - display: flex; - flex-direction: column; - gap: 0.35rem; - z-index: 5; -} - -${scopeSelector} .org-menu-option + .org-menu-option { - margin-left: 0; -} - -${scopeSelector} .org-menu-option { - width: 100%; - justify-content: flex-start; - background: transparent; - color: var(--text-main); - border: 1px solid transparent; -} - -${scopeSelector} .org-menu-option:hover { - background: #f8fafc; - border-color: rgb(148 163 184 / 0.35); -} - -${scopeSelector} .org-menu-option.is-active { - background: rgb(226 232 240 / 0.7); - color: var(--primary-hover); - border-color: rgb(148 163 184 / 0.35); -} - -${scopeSelector} .org-finance-meta { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - margin-bottom: 1.5rem; -} - -${scopeSelector} .org-finance-meta > div { - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -${scopeSelector} .org-meta-label { - font-size: 0.76rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-action-grid { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1rem; -} - -${scopeSelector} .org-action-grid button + button { - margin-left: 0; -} - -${scopeSelector} .org-action-grid button { - width: 100%; -} - -${scopeSelector} .org-access-note { - margin: 0 0 1rem; - color: var(--text-muted); - font-size: 0.95rem; -} - -${scopeSelector} .org-credit-summary { - display: flex; - flex-direction: column; - gap: 0.25rem; - padding: 0.85rem 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-credit-summary strong { - font-size: 1rem; -} - -${scopeSelector} .org-credit-summary span:last-child { - font-size: 0.92rem; - line-height: 1.45; -} - -${scopeSelector} .org-credit-lines-list { - display: flex; - flex-direction: column; - gap: 0.85rem; -} - -${scopeSelector} .org-credit-line-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-credit-line-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-credit-line-member { - display: flex; - flex-direction: column; - gap: 0.3rem; -} - -${scopeSelector} .org-credit-line-label { - font-size: 0.76rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-credit-line-empty { - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - color: var(--text-muted); -} - -@media (max-width: 960px) { - ${scopeSelector} .org-finance-meta { - grid-template-columns: 1fr; - } - - ${scopeSelector} .org-credit-line-row { - flex-direction: column; - align-items: flex-start; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.TreasuryCard = function TreasuryCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const creditLines = store.getCreditLines(); - const allowTreasuryActions = permissions.canManageTreasury(); - const activeTab = getTreasuryTab(); - const isMenuOpen = getTreasuryMenuOpen(); - const activeCreditLabel = - creditLines.length === 1 - ? "1 active credit line" - : `${creditLines.length} active credit lines`; - ensureScopedStyle("portal-treasury-card", treasuryCardCss); - - return PanelCard({ - className: "org-span-5", - title: "Treasury", - subtitle: "Organization funds, reputation, and member payouts.", - headerExtras: h( - "div", - { className: "org-treasury-menu" }, - h( - "button", - { - type: "button", - className: "org-menu-btn", - title: "Treasury views", - "aria-label": "Treasury views", - onClick: () => setTreasuryMenuOpen((open) => !open), - }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "aria-hidden": "true", - }, - h("line", { x1: "4", y1: "7", x2: "20", y2: "7" }), - h("line", { x1: "4", y1: "12", x2: "20", y2: "12" }), - h("line", { x1: "4", y1: "17", x2: "20", y2: "17" }), - ), - ), - isMenuOpen - ? h( - "div", - { className: "org-menu-dropdown" }, - h( - "button", - { - type: "button", - className: - activeTab === "overview" - ? "org-menu-option is-active" - : "org-menu-option", - onClick: () => { - setTreasuryTab("overview"); - setTreasuryMenuOpen(false); - }, - }, - "Overview", - ), - h( - "button", - { - type: "button", - className: - activeTab === "credit" - ? "org-menu-option is-active" - : "org-menu-option", - onClick: () => { - setTreasuryTab("credit"); - setTreasuryMenuOpen(false); - }, - }, - "Credit Lines", - ), - ) - : null, - ), - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - null, - activeTab === "credit" - ? creditLines.length > 0 - ? h( - "div", - { className: "org-credit-lines-list" }, - ...creditLines.map((line) => - h( - "article", - { className: "org-credit-line-row" }, - h( - "div", - { - className: - "org-credit-line-member", - }, - h( - "span", - { - className: - "org-credit-line-label", - }, - "Member", - ), - h("strong", null, line.member), - ), - h( - "div", - { - className: - "org-credit-line-member", - }, - h( - "span", - { - className: - "org-credit-line-label", - }, - "Amount", - ), - h( - "strong", - null, - actions.formatCurrency( - line.amount, - ), - ), - ), - ), - ), - ) - : h( - "div", - { className: "org-credit-line-empty" }, - "No active credit lines.", - ) - : h( - "div", - null, - h( - "div", - { className: "org-finance-meta" }, - h( - "div", - null, - h( - "span", - { className: "org-meta-label" }, - "Funds", - ), - h( - "strong", - null, - actions.formatCurrency(store.getFunds()), - ), - ), - h( - "div", - null, - h( - "span", - { className: "org-meta-label" }, - "Reputation", - ), - h("strong", null, `${portalData.reputation}`), - ), - ), - allowTreasuryActions - ? h( - "div", - { className: "org-action-grid" }, - h( - "button", - { - type: "button", - onClick: () => - actions.openModal("payroll"), - }, - "Run Payroll", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => - actions.openModal("transfer"), - }, - "Send Funds", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => - actions.openModal("credit"), - }, - "Credit Line", - ), - ) - : h( - "p", - { className: "org-access-note" }, - "Only the organization leader or CEO can manage treasury actions.", - ), - h( - "div", - { className: "org-credit-summary" }, - h( - "span", - { className: "org-meta-label" }, - "Credit Line Status", - ), - h("strong", null, activeCreditLabel), - h( - "span", - null, - creditLines.length > 0 - ? "Open the Credit Lines tab to review assigned members and amounts." - : "Assign a credit line to create the first approved member limit.", - ), - ), - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/controls.css b/arma/ui/apps/controls.css deleted file mode 100644 index 5ed9592..0000000 --- a/arma/ui/apps/controls.css +++ /dev/null @@ -1,33 +0,0 @@ -.org-secondary-btn { - background: var(--bg-surface); - color: var(--text-main); - border: 1px solid var(--border); - - &:hover { - background: var(--bg-surface-hover); - color: var(--text-main); - } -} - -.org-danger-btn { - background: #7f1d1d; - color: #fef2f2; - - &:hover { - background: #991b1b; - } -} - -.org-icon-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 2.5rem; - height: 2.5rem; - padding: 0; -} - -.org-icon { - width: 1rem; - height: 1rem; -} diff --git a/arma/ui/apps/hero.css b/arma/ui/apps/hero.css deleted file mode 100644 index eaafcae..0000000 --- a/arma/ui/apps/hero.css +++ /dev/null @@ -1,41 +0,0 @@ -.org-page-header { - text-align: left; - margin-bottom: 0; -} - -.org-page-heading { - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.org-page-kicker { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); - font-weight: 600; -} - -.org-page-title { - margin: 0; -} - -.org-page-subtitle { - font-size: 0.9rem; - color: var(--text-muted); - margin: 0; -} - -.org-page-meta { - font-size: 0.75rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -@media (max-width: 960px) { - .org-page-heading { - gap: 0.3rem; - } -} diff --git a/arma/ui/apps/logic/portalActions.js b/arma/ui/apps/logic/portalActions.js deleted file mode 100644 index bb9e9d1..0000000 --- a/arma/ui/apps/logic/portalActions.js +++ /dev/null @@ -1,318 +0,0 @@ -(function () { - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - SharedLogic.createPortalActions = function createPortalActions({ - portalData, - store, - permissions, - registryStore, - }) { - class OrgPortalActions { - constructor() { - this.treasuryNoticeTimer = null; - } - - formatCurrency(value) { - return "$" + value.toLocaleString(); - } - - formatVehicleType(type) { - if (!type) { - return ""; - } - - return type.charAt(0).toUpperCase() + type.slice(1); - } - - formatAssetType(type) { - if (!type) { - return ""; - } - - return type.charAt(0).toUpperCase() + type.slice(1); - } - - formatDisplayName(value) { - if (!value) { - return ""; - } - - return String(value) - .trim() - .split(/\s+/) - .map((part) => { - if (!part) { - return ""; - } - - return ( - part.charAt(0).toUpperCase() + - part.slice(1).toLowerCase() - ); - }) - .join(" "); - } - - getAssetReadiness() { - if (portalData.fleet.length === 0) { - return null; - } - - const total = portalData.fleet.reduce( - (sum, unit) => sum + (100 - parseInt(unit.damage, 10)), - 0, - ); - return Math.round(total / portalData.fleet.length); - } - - showTreasuryNotice(type, text) { - store.setTreasuryNotice({ type, text }); - - if (this.treasuryNoticeTimer) { - clearTimeout(this.treasuryNoticeTimer); - } - - this.treasuryNoticeTimer = setTimeout(() => { - store.setTreasuryNotice({ type: "", text: "" }); - this.treasuryNoticeTimer = null; - }, 3500); - } - - parseAmount(value) { - const amount = Number(value); - return Number.isFinite(amount) ? Math.round(amount) : 0; - } - - getInputValue(id) { - const el = document.getElementById(id); - return el ? el.value : ""; - } - - isOwnerMember(memberName) { - return ( - String(memberName || "") - .trim() - .toLowerCase() === - String(portalData.org.owner || "") - .trim() - .toLowerCase() - ); - } - - closePortal() { - if ( - typeof A3API !== "undefined" && - typeof A3API.SendAlert === "function" - ) { - A3API.SendAlert( - JSON.stringify({ - event: "org::close", - data: {}, - }), - ); - return; - } - - if (registryStore) { - registryStore.setView("home"); - } - } - - openModal(type) { - if ( - (type === "payroll" || - type === "transfer" || - type === "credit") && - !permissions.canManageTreasury() - ) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return; - } - - if (type === "disband" && !permissions.canDisbandOrg()) { - return; - } - - store.setModal({ type }); - } - - closeModal() { - store.setModal(null); - } - - removeMember(memberName) { - if (!permissions.canManageMembers()) { - return false; - } - - if (this.isOwnerMember(memberName)) { - return false; - } - - store.setMembers((currentMembers) => - currentMembers.filter( - (member) => member.name !== memberName, - ), - ); - store.setCreditLines((currentLines) => - currentLines.filter((line) => line.member !== memberName), - ); - return true; - } - - disbandOrganization() { - if (!permissions.canDisbandOrg()) { - return false; - } - - store.setOrgDisbanded(true); - this.closeModal(); - return true; - } - - runPayroll(amountPerMember) { - if (!permissions.canManageTreasury()) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return false; - } - - const members = store.getMembers(); - const funds = store.getFunds(); - - if (members.length === 0) { - this.showTreasuryNotice( - "error", - "No members available for payroll.", - ); - return false; - } - - if (amountPerMember <= 0) { - this.showTreasuryNotice( - "error", - "Enter a valid payroll amount.", - ); - return false; - } - - const total = amountPerMember * members.length; - if (total > funds) { - this.showTreasuryNotice( - "error", - "Insufficient org funds for payroll.", - ); - return false; - } - - store.setFunds(funds - total); - this.showTreasuryNotice( - "success", - `Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`, - ); - return true; - } - - sendFundsToMember(memberName, amount) { - if (!permissions.canManageTreasury()) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return false; - } - - const funds = store.getFunds(); - - if (!memberName) { - this.showTreasuryNotice( - "error", - "Select a member to receive funds.", - ); - return false; - } - - if (amount <= 0) { - this.showTreasuryNotice( - "error", - "Enter a valid transfer amount.", - ); - return false; - } - - if (amount > funds) { - this.showTreasuryNotice( - "error", - "Insufficient org funds for this transfer.", - ); - return false; - } - - store.setFunds(funds - amount); - this.showTreasuryNotice( - "success", - `${this.formatCurrency(amount)} sent to ${memberName}.`, - ); - return true; - } - - grantCreditLine(memberName, amount) { - if (!permissions.canManageTreasury()) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return false; - } - - if (!memberName) { - this.showTreasuryNotice( - "error", - "Select a member for the credit line.", - ); - return false; - } - - if (amount <= 0) { - this.showTreasuryNotice( - "error", - "Enter a valid credit line amount.", - ); - return false; - } - - store.setCreditLines((currentLines) => { - const existingIndex = currentLines.findIndex( - (line) => line.member === memberName, - ); - if (existingIndex === -1) { - return [ - ...currentLines, - { member: memberName, amount }, - ]; - } - - const updatedLines = [...currentLines]; - updatedLines[existingIndex] = { - member: memberName, - amount, - }; - return updatedLines; - }); - - this.showTreasuryNotice( - "success", - `Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`, - ); - return true; - } - } - - return new OrgPortalActions(); - }; -})(); diff --git a/arma/ui/apps/logic/portalPermissions.js b/arma/ui/apps/logic/portalPermissions.js deleted file mode 100644 index b454363..0000000 --- a/arma/ui/apps/logic/portalPermissions.js +++ /dev/null @@ -1,75 +0,0 @@ -(function () { - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - SharedLogic.createPortalPermissions = function createPortalPermissions({ - portalData, - session, - }) { - class OrgPortalPermissions { - getNormalizedRole() { - return String(session.role || "") - .trim() - .toUpperCase(); - } - - isDefaultOrg() { - return ( - portalData.org.isDefault === true || - String(portalData.org.tag || "") - .trim() - .toUpperCase() === "DEFAULT" - ); - } - - isOrgOwner() { - const ownerUid = String( - portalData.org.ownerUid || portalData.org.owner || "", - ) - .trim() - .toLowerCase(); - const actorUid = String(session.actorUid || "") - .trim() - .toLowerCase(); - - if (ownerUid && actorUid) { - return actorUid === ownerUid; - } - - return ( - String(session.actorName || "") - .trim() - .toLowerCase() === - String(portalData.org.owner || "") - .trim() - .toLowerCase() - ); - } - - isSessionCeo() { - return session.ceo === true; - } - - isOrgLeaderOrCeo() { - return ( - this.isOrgOwner() || - this.getNormalizedRole() === "LEADER" || - (this.isDefaultOrg() && this.isSessionCeo()) - ); - } - - canManageMembers() { - return this.isOrgLeaderOrCeo(); - } - - canManageTreasury() { - return this.isOrgLeaderOrCeo(); - } - - canDisbandOrg() { - return this.isOrgLeaderOrCeo(); - } - } - - return new OrgPortalPermissions(); - }; -})(); diff --git a/arma/ui/apps/logic/portalStore.js b/arma/ui/apps/logic/portalStore.js deleted file mode 100644 index 99b8a19..0000000 --- a/arma/ui/apps/logic/portalStore.js +++ /dev/null @@ -1,38 +0,0 @@ -(function () { - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - SharedLogic.createPortalStore = function createPortalStore({ - createSignal, - portalData, - }) { - class OrgPortalStore { - constructor() { - [this.getFunds, this.setFunds] = createSignal(portalData.funds); - [this.getMembers, this.setMembers] = createSignal([ - ...portalData.members, - ]); - [this.getCreditLines, this.setCreditLines] = createSignal([]); - [this.getTreasuryNotice, this.setTreasuryNotice] = createSignal( - { - type: "", - text: "", - }, - ); - [this.getModal, this.setModal] = createSignal(null); - [this.getOrgDisbanded, this.setOrgDisbanded] = - createSignal(false); - } - - hydrateFromPayload(payload) { - this.setFunds(payload.portalData.funds || 0); - this.setMembers([...(payload.portalData.members || [])]); - this.setCreditLines([]); - this.setTreasuryNotice({ type: "", text: "" }); - this.setModal(null); - this.setOrgDisbanded(false); - } - } - - return new OrgPortalStore(); - }; -})(); diff --git a/arma/ui/apps/logic/registryStore.js b/arma/ui/apps/logic/registryStore.js deleted file mode 100644 index e7db429..0000000 --- a/arma/ui/apps/logic/registryStore.js +++ /dev/null @@ -1,69 +0,0 @@ -(function () { - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - SharedLogic.createRegistryStore = function createRegistryStore({ - createSignal, - onHydratePortal, - }) { - class RegistryStore { - constructor() { - [this.getView, this.setView] = createSignal("home"); - [this.getIsAuthenticating, this.setIsAuthenticating] = - createSignal(false); - [this.getLoginError, this.setLoginError] = createSignal(""); - [this.getIsCreating, this.setIsCreating] = createSignal(false); - [this.getCreateError, this.setCreateError] = createSignal(""); - } - - startLogin() { - this.setLoginError(""); - this.setIsAuthenticating(true); - } - - startCreate() { - this.setCreateError(""); - this.setIsCreating(true); - } - - failLogin(message) { - this.setIsAuthenticating(false); - this.setLoginError(message || "Authentication failed."); - } - - failCreate(message) { - this.setIsCreating(false); - this.setCreateError( - message || "Organization registration failed.", - ); - } - - hydratePortal(payload) { - return Boolean(onHydratePortal && onHydratePortal(payload)); - } - - completeLogin(payload) { - if (!this.hydratePortal(payload)) { - this.failLogin("Login response was missing portal data."); - return; - } - - this.setLoginError(""); - this.setIsAuthenticating(false); - } - - completeCreate(payload) { - if (!this.hydratePortal(payload)) { - this.failCreate( - "Organization registration response was missing portal data.", - ); - return; - } - - this.setCreateError(""); - this.setIsCreating(false); - } - } - - return new RegistryStore(); - }; -})(); diff --git a/arma/ui/apps/main/bootstrap.js b/arma/ui/apps/main/bootstrap.js deleted file mode 100644 index 663a0b3..0000000 --- a/arma/ui/apps/main/bootstrap.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Registry app bootstrap - */ - -const root = document.getElementById("app"); -window.RegistryApp.runtime.render(window.RegistryApp.components.App, root); diff --git a/arma/ui/apps/main/bridge.js b/arma/ui/apps/main/bridge.js deleted file mode 100644 index 837ec0c..0000000 --- a/arma/ui/apps/main/bridge.js +++ /dev/null @@ -1,123 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const store = RegistryApp.store; - - function sendEvent(event, data) { - if ( - typeof A3API !== "undefined" && - typeof A3API.SendAlert === "function" - ) { - A3API.SendAlert( - JSON.stringify({ - event, - data, - }), - ); - return true; - } - - return false; - } - - function getMockPayload() { - const OrgPortal = window.OrgPortal; - return { - session: JSON.parse(JSON.stringify(OrgPortal.data.session)), - portalData: JSON.parse(JSON.stringify(OrgPortal.data.portalData)), - }; - } - - function requestLogin(credentials) { - store.startLogin(); - - const sent = sendEvent("org::login::request", credentials); - if (sent) { - return; - } - - window.setTimeout(() => { - store.completeLogin(getMockPayload()); - }, 350); - } - - function requestCreateOrg(registration) { - store.startCreate(); - - const sent = sendEvent("org::create::request", registration); - if (sent) { - return; - } - - window.setTimeout(() => { - const orgName = String(registration.orgName || "").trim(); - if (!orgName) { - store.failCreate("Enter an organization name."); - return; - } - - const payload = getMockPayload(); - payload.portalData.org.name = orgName; - payload.portalData.org.tag = String(Date.now()).slice(-10); - payload.portalData.org.owner = - payload.session.actorName || "Unknown"; - payload.portalData.org.ownerUid = payload.session.actorUid || ""; - payload.portalData.org.isDefault = false; - payload.session.role = "Leader"; - payload.session.ceo = false; - payload.portalData.members = [ - { name: payload.session.actorName || "Unknown" }, - ]; - - store.completeCreate(payload); - }, 350); - } - - function receive(eventOrPayload, data = {}) { - const event = - typeof eventOrPayload === "object" && eventOrPayload !== null - ? eventOrPayload.event - : eventOrPayload; - const payloadData = - typeof eventOrPayload === "object" && eventOrPayload !== null - ? eventOrPayload.data || {} - : data; - - if (event === "org::login::success") { - store.completeLogin(payloadData); - return; - } - - if (event === "org::login::failure") { - store.failLogin(payloadData.message || "Authentication failed."); - return; - } - - if (event === "org::create::success") { - store.completeCreate(payloadData); - return; - } - - if (event === "org::create::failure") { - store.failCreate( - payloadData.message || "Organization registration failed.", - ); - } - } - - RegistryApp.bridge = { - requestLogin, - requestCreateOrg, - receive, - sendEvent, - }; - - window.OrgUIBridge = { - requestLogin, - requestCreateOrg, - receive, - receiveLoginSuccess: (data) => receive("org::login::success", data), - receiveLoginFailure: (data) => receive("org::login::failure", data), - receiveCreateSuccess: (data) => receive("org::create::success", data), - receiveCreateFailure: (data) => receive("org::create::failure", data), - }; -})(); diff --git a/arma/ui/apps/main/index.html b/arma/ui/apps/main/index.html deleted file mode 100644 index a699380..0000000 --- a/arma/ui/apps/main/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - ORBIS - Global Organization Network - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/arma/ui/apps/main/state.js b/arma/ui/apps/main/state.js deleted file mode 100644 index c4c0106..0000000 --- a/arma/ui/apps/main/state.js +++ /dev/null @@ -1,23 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { createSignal } = RegistryApp.runtime; - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - RegistryApp.store = SharedLogic.createRegistryStore({ - createSignal, - onHydratePortal(payload) { - const OrgPortal = window.OrgPortal; - const portalData = payload?.portalData; - const session = payload?.session; - - if (!OrgPortal || !portalData || !session) { - return false; - } - - OrgPortal.data.applyLoginPayload(payload); - OrgPortal.store.hydrateFromPayload(payload); - RegistryApp.store.setView("portal"); - return true; - }, - }); -})(); diff --git a/arma/ui/apps/portal/actions.js b/arma/ui/apps/portal/actions.js deleted file mode 100644 index b8b3f5e..0000000 --- a/arma/ui/apps/portal/actions.js +++ /dev/null @@ -1,15 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const permissions = OrgPortal.permissions; - const registryStore = window.RegistryApp.store; - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - OrgPortal.actions = SharedLogic.createPortalActions({ - portalData, - store, - permissions, - registryStore, - }); -})(); diff --git a/arma/ui/apps/portal/data.js b/arma/ui/apps/portal/data.js deleted file mode 100644 index 66208f0..0000000 --- a/arma/ui/apps/portal/data.js +++ /dev/null @@ -1,172 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const staticOrgProfile = { - type: "Organization", - status: "Operational", - headquarters: "ArmA Verse", - }; - - function cloneValue(value) { - return JSON.parse(JSON.stringify(value)); - } - - function replaceObject(target, source) { - Object.keys(target).forEach((key) => delete target[key]); - Object.assign(target, cloneValue(source)); - } - - function replaceArray(target, source) { - target.splice(0, target.length, ...cloneValue(source)); - } - - OrgPortal.data = { - portalData: { - org: Object.assign( - { - name: "Black Rifle Company", - tag: "BRC-0160566824", - owner: "Jacob Schmidt", - ownerUid: "uid-jacob-schmidt", - isDefault: false, - }, - staticOrgProfile, - ), - funds: 482750, - reputation: 72, - members: [ - { name: "Jacob Schmidt" }, - { name: "Mara Velez" }, - { name: "Rylan Cross" }, - { name: "Noah Briggs" }, - { name: "Elena Price" }, - { name: "Isaac Rowe" }, - { name: "Talia Boone" }, - { name: "Cade Mercer" }, - ], - fleet: [ - { - name: "UH-80 Ghost Hawk", - type: "helicopter", - status: "Ready", - damage: "16%", - }, - { - name: "MH-9 Hummingbird", - type: "helicopter", - status: "Ready", - damage: "8%", - }, - { - name: "M-ATV Patrol 1", - type: "car", - status: "Fielded", - damage: "24%", - }, - { - name: "M2A1 Slammer", - type: "armor", - status: "Ready", - damage: "11%", - }, - { - name: "RHIB Patrol Boat", - type: "naval", - status: "Repairing", - damage: "32%", - }, - ], - assets: [ - { name: "First Aid Kits", type: "items", quantity: "36" }, - { name: "MX 6.5 mm Rifles", type: "weapons", quantity: "18" }, - { - name: "6.5 mm Magazines", - type: "magazines", - quantity: "120", - }, - { - name: "Carryall Backpacks", - type: "backpacks", - quantity: "24", - }, - ], - activity: [ - { - time: "08:20", - text: "Treasury cleared contractor payment for northern route escort.", - }, - { - time: "07:45", - text: "Viper Flight completed readiness checks on all rotary assets.", - }, - { - time: "07:10", - text: "New recruit Cade Mercer accepted into ground training roster.", - }, - { - time: "06:30", - text: "North Depot inventory count pushed reserve ratio above target.", - }, - ], - roadmap: [ - { - name: "Contracts Board", - status: "Planned", - detail: "Track payouts, assignments, and claim approvals.", - }, - { - name: "Diplomacy", - status: "Future Review", - detail: "Possible future module pending a full design and scope review.", - }, - { - name: "Logistics Queue", - status: "Future Review", - detail: "Possible future module pending a full design and scope review.", - }, - { - name: "Permissions", - status: "Future Review", - detail: "Possible future module pending a full design and scope review.", - }, - ], - }, - session: { - actorName: "Jacob Schmidt", - actorUid: "uid-jacob-schmidt", - role: "Leader", - ceo: false, - }, - applyLoginPayload(payload) { - replaceObject( - this.portalData.org, - Object.assign( - {}, - payload.portalData.org || {}, - staticOrgProfile, - ), - ); - this.portalData.funds = payload.portalData.funds || 0; - this.portalData.reputation = payload.portalData.reputation || 0; - - replaceArray( - this.portalData.members, - payload.portalData.members || [], - ); - replaceArray(this.portalData.fleet, payload.portalData.fleet || []); - replaceArray( - this.portalData.assets, - payload.portalData.assets || [], - ); - replaceArray( - this.portalData.activity, - payload.portalData.activity || [], - ); - replaceArray( - this.portalData.roadmap, - payload.portalData.roadmap || [], - ); - - replaceObject(this.session, payload.session || {}); - }, - }; -})(); diff --git a/arma/ui/apps/portal/permissions.js b/arma/ui/apps/portal/permissions.js deleted file mode 100644 index b25ec78..0000000 --- a/arma/ui/apps/portal/permissions.js +++ /dev/null @@ -1,10 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { portalData, session } = OrgPortal.data; - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - OrgPortal.permissions = SharedLogic.createPortalPermissions({ - portalData, - session, - }); -})(); diff --git a/arma/ui/apps/portal/store.js b/arma/ui/apps/portal/store.js deleted file mode 100644 index 5f2711e..0000000 --- a/arma/ui/apps/portal/store.js +++ /dev/null @@ -1,11 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { createSignal } = window.RegistryApp.runtime; - const { portalData } = OrgPortal.data; - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - OrgPortal.store = SharedLogic.createPortalStore({ - createSignal, - portalData, - }); -})(); diff --git a/arma/ui/apps/runtime.js b/arma/ui/apps/runtime.js deleted file mode 100644 index fce44e9..0000000 --- a/arma/ui/apps/runtime.js +++ /dev/null @@ -1,122 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - - const SVG_NS = "http://www.w3.org/2000/svg"; - const SVG_TAGS = new Set([ - "svg", - "path", - "circle", - "rect", - "line", - "polyline", - "polygon", - "g", - "defs", - "use", - "text", - "tspan", - "clipPath", - "mask", - ]); - - function h(tag, props = {}, ...children) { - const isSvg = SVG_TAGS.has(tag); - const el = isSvg - ? document.createElementNS(SVG_NS, tag) - : document.createElement(tag); - - if (props) { - Object.entries(props).forEach(([key, value]) => { - if (key.startsWith("on") && typeof value === "function") { - el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === "className") { - if (isSvg) { - el.setAttribute("class", value); - } else { - el.className = value; - } - } else if (key === "style" && typeof value === "object") { - Object.assign(el.style, value); - } else if (typeof value === "boolean") { - if (value) { - el.setAttribute(key, ""); - } else { - el.removeAttribute(key); - } - } else if (value === null || value === undefined) { - el.removeAttribute(key); - } else { - el.setAttribute(key, value); - } - }); - } - - children.forEach((child) => { - if (typeof child === "string" || typeof child === "number") { - el.appendChild(document.createTextNode(child)); - } else if (child instanceof Node) { - el.appendChild(child); - } else if (Array.isArray(child)) { - child.forEach((c) => el.appendChild(c)); - } - }); - - return el; - } - - let rootContainer = null; - let rootComponent = null; - const injectedStyles = new Set(); - - function render(component, container) { - rootContainer = container; - rootComponent = component; - rerender(); - } - - function rerender() { - if (!rootContainer || !rootComponent) { - return; - } - - rootContainer.innerHTML = ""; - rootContainer.appendChild(rootComponent()); - } - - function ensureScopedStyle(id, cssText) { - if (!id || !cssText || injectedStyles.has(id)) { - return; - } - - const style = document.createElement("style"); - style.setAttribute("data-ui-style", id); - style.textContent = cssText; - document.head.appendChild(style); - injectedStyles.add(id); - } - - function createSignal(initialValue) { - let value = initialValue; - - const getValue = () => value; - const setValue = (newValue) => { - value = typeof newValue === "function" ? newValue(value) : newValue; - rerender(); - }; - - return [getValue, setValue]; - } - - const runtime = { - h, - render, - createSignal, - ensureScopedStyle, - rerender, - }; - - RegistryApp.runtime = runtime; - OrgPortal.runtime = runtime; - window.AppRuntime = runtime; -})(); diff --git a/arma/ui/apps/views/DisbandedView.js b/arma/ui/apps/views/DisbandedView.js deleted file mode 100644 index 01659e0..0000000 --- a/arma/ui/apps/views/DisbandedView.js +++ /dev/null @@ -1,36 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const registryStore = window.RegistryApp.store; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.DisbandedView = function DisbandedView() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - - return PanelCard({ - className: "org-span-12 org-empty-state", - eyebrow: "Organization Removed", - title: portalData.org.name, - body: h( - "div", - null, - h( - "p", - { className: "org-summary" }, - "This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => registryStore.setView("home"), - }, - "Return to Registry", - ), - ), - }); - }; -})(); diff --git a/arma/ui/apps/views/HomeView.js b/arma/ui/apps/views/HomeView.js deleted file mode 100644 index be9a07b..0000000 --- a/arma/ui/apps/views/HomeView.js +++ /dev/null @@ -1,97 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const store = RegistryApp.store; - const bridge = RegistryApp.bridge; - const scopeAttr = "data-ui-home-view"; - const scopeSelector = `[${scopeAttr}]`; - const homeViewCss = ` -${scopeSelector} { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin-bottom: 2rem; -} - -${scopeSelector} .home-span-full { - grid-column: span 2; -} - -${scopeSelector} .home-feedback { - padding: 0.85rem 1rem; - border-radius: var(--radius); - font-size: 0.92rem; - background: #fef2f2; - border: 1px solid #fecaca; - color: #991b1b; -} - -@media (max-width: 960px) { - ${scopeSelector} { - grid-template-columns: 1fr; - } - - ${scopeSelector} .home-span-full { - grid-column: span 1; - } -} -`; - - RegistryApp.componentFns = RegistryApp.componentFns || {}; - - RegistryApp.componentFns.HomeView = function HomeView() { - const isAuthenticating = store.getIsAuthenticating(); - const loginError = store.getLoginError(); - ensureScopedStyle("main-home-view", homeViewCss); - - return h( - "div", - { className: "content", [scopeAttr]: "" }, - h( - "div", - { className: "card" }, - h("h2", null, "Create Organization"), - h( - "p", - null, - "Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.", - ), - h( - "button", - { onClick: () => store.setView("create") }, - "Register", - ), - ), - h( - "div", - { className: "card" }, - h("h2", null, "Organization Portal"), - h( - "p", - null, - "Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.", - ), - loginError - ? h("div", { className: "home-feedback" }, loginError) - : null, - h( - "button", - { - disabled: isAuthenticating, - onClick: () => { - if (!bridge) { - store.failLogin( - "Login bridge is not available.", - ); - return; - } - - bridge.requestLogin({}); - }, - }, - isAuthenticating ? "Opening Portal..." : "Login", - ), - ), - ); - }; -})(); diff --git a/arma/ui/apps/views/PortalView.js b/arma/ui/apps/views/PortalView.js deleted file mode 100644 index 8bf61b8..0000000 --- a/arma/ui/apps/views/PortalView.js +++ /dev/null @@ -1,206 +0,0 @@ -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData, session } = OrgPortal.data; - const store = OrgPortal.store; - const portalViewScope = "[data-ui-portal-view]"; - - ensureScopedStyle( - "portal-view", - ` - ${portalViewScope} .org-toast-stack { - position: fixed; - top: 1.5rem; - right: 2rem; - z-index: 20; - display: flex; - flex-direction: column; - gap: 0.75rem; - pointer-events: none; - } - - ${portalViewScope} .org-toast { - max-width: 24rem; - padding: 0.9rem 1rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: #fff; - box-shadow: 0 12px 28px rgb(15 23 42 / 0.14); - font-size: 0.92rem; - pointer-events: auto; - } - - ${portalViewScope} .org-toast.is-success { - background: #ecfdf5; - border-color: #bbf7d0; - color: #166534; - } - - ${portalViewScope} .org-toast.is-error { - background: #fef2f2; - border-color: #fecaca; - color: #991b1b; - } - - ${portalViewScope} .org-dashboard-grid { - display: grid; - grid-template-columns: repeat(12, minmax(0, 1fr)); - gap: 1.5rem; - } - - ${portalViewScope} .org-panel { - margin-bottom: 0; - text-align: left; - } - - ${portalViewScope} .org-scroll-panel { - display: flex; - flex-direction: column; - max-height: 31rem; - overflow: hidden; - } - - ${portalViewScope} .org-span-12 { - grid-column: span 12; - } - - ${portalViewScope} .org-span-7 { - grid-column: span 7; - } - - ${portalViewScope} .org-span-6 { - grid-column: span 6; - } - - ${portalViewScope} .org-span-5 { - grid-column: span 5; - } - - @media (max-width: 960px) { - ${portalViewScope} .org-toast-stack { - top: 1rem; - right: 1rem; - left: 1rem; - } - - ${portalViewScope} .org-toast { - max-width: none; - } - - ${portalViewScope} .org-span-12, - ${portalViewScope} .org-span-7, - ${portalViewScope} .org-span-6, - ${portalViewScope} .org-span-5 { - grid-column: span 12; - } - } - `, - ); - - OrgPortal.components = OrgPortal.components || {}; - - OrgPortal.components.App = function App() { - const Hero = window.SharedUI.componentFns.Hero; - const Footer = window.SharedUI.componentFns.Footer; - const OverviewCard = OrgPortal.componentFns.OverviewCard; - const FleetCard = OrgPortal.componentFns.FleetCard; - const TreasuryCard = OrgPortal.componentFns.TreasuryCard; - const MembersCard = OrgPortal.componentFns.MembersCard; - const AssetsCard = OrgPortal.componentFns.AssetsCard; - const ActivityCard = OrgPortal.componentFns.ActivityCard; - const FutureCard = OrgPortal.componentFns.FutureCard; - const DangerCard = OrgPortal.componentFns.DangerCard; - const ModalLayer = OrgPortal.componentFns.ModalLayer; - const DisbandedView = OrgPortal.componentFns.DisbandedView; - const treasuryNotice = store.getTreasuryNotice(); - const footerSections = [ - { - title: "Organization Controls", - items: [ - "Roster Management", - "Fleet Assignment", - "Treasury Permissions", - "Asset Registry", - ], - }, - { - title: "Planned Extensions", - items: [ - "Contracts Board", - "Diplomacy Layer", - "Procurement Queue", - "Reputation History", - ], - }, - ]; - - if (store.getOrgDisbanded()) { - return h( - "main", - { "data-ui-portal-view": "" }, - h( - "div", - { className: "container" }, - h( - "div", - { className: "org-dashboard-grid" }, - Hero({ - kicker: portalData.org.tag, - title: portalData.org.name, - subtitle: "Player organization command portal", - meta: `${session.actorName} - ${session.role}`, - }), - DisbandedView(), - ), - ), - ModalLayer(), - Footer({ sections: footerSections }), - ); - } - - return h( - "main", - { "data-ui-portal-view": "" }, - treasuryNotice.text - ? h( - "div", - { className: "org-toast-stack" }, - h( - "div", - { - className: - treasuryNotice.type === "error" - ? "org-toast is-error" - : "org-toast is-success", - }, - treasuryNotice.text, - ), - ) - : null, - h( - "div", - { className: "container" }, - h( - "div", - { className: "org-dashboard-grid" }, - Hero({ - kicker: portalData.org.tag, - title: portalData.org.name, - subtitle: "Player organization command portal", - meta: `${session.actorName} - ${session.role}`, - }), - OverviewCard(), - FleetCard(), - TreasuryCard(), - MembersCard(), - AssetsCard(), - ActivityCard(), - FutureCard(), - DangerCard(), - ), - ), - ModalLayer(), - Footer({ sections: footerSections }), - ); - }; -})(); diff --git a/arma/ui/apps/views/RegistrationView.js b/arma/ui/apps/views/RegistrationView.js deleted file mode 100644 index 841abf0..0000000 --- a/arma/ui/apps/views/RegistrationView.js +++ /dev/null @@ -1,342 +0,0 @@ -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const store = RegistryApp.store; - const bridge = RegistryApp.bridge; - const scopeAttr = "data-ui-registration-view"; - const scopeSelector = `[${scopeAttr}]`; - const registrationViewCss = ` -${scopeSelector} { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: center; - width: 100%; -} - -${scopeSelector} .info-panel { - text-align: left; - padding: 1rem; -} - -${scopeSelector} .create-feature-list { - text-align: left; - margin-top: 1.5rem; - list-style-type: none; - padding: 0; -} - -${scopeSelector} .create-feature-item { - margin-bottom: 0.5rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -${scopeSelector} .create-feature-icon { - width: 1.2rem; - height: 1.2rem; - flex-shrink: 0; -} - -${scopeSelector} .price-tag { - margin-top: 2rem; - padding: 1rem; - background: var(--bg-app); - border-radius: var(--radius); - border: 1px solid var(--border); -} - -${scopeSelector} .price-label { - display: block; - font-size: 0.9rem; - color: var(--text-muted); -} - -${scopeSelector} .price-value { - display: block; - font-size: 2rem; - font-weight: 700; - color: var(--primary); -} - -${scopeSelector} .form-panel { - margin: 0; -} - -${scopeSelector} .app-form { - display: flex; - flex-direction: column; - gap: 1rem; - text-align: left; -} - -${scopeSelector} .app-form label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; -} - -${scopeSelector} .app-form input, -${scopeSelector} .app-form select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: border-color 0.2s; -} - -${scopeSelector} .app-form input:focus, -${scopeSelector} .app-form select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1); -} - -${scopeSelector} .form-actions { - margin-top: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; -} - -${scopeSelector} .submit-btn { - width: 100%; -} - -${scopeSelector} .cancel-link { - font-size: 0.9rem; - color: var(--text-muted); - cursor: pointer; - text-decoration: underline; -} - -${scopeSelector} .cancel-link:hover { - color: var(--primary); -} - -${scopeSelector} .form-feedback { - padding: 0.85rem 1rem; - border-radius: var(--radius); - font-size: 0.92rem; -} - -${scopeSelector} .form-feedback.is-error { - background: #fef2f2; - border: 1px solid #fecaca; - color: #991b1b; -} - -@media (max-width: 960px) { - ${scopeSelector} { - grid-template-columns: 1fr; - } -} -`; - - RegistryApp.componentFns = RegistryApp.componentFns || {}; - - RegistryApp.componentFns.RegistrationView = function RegistrationView() { - const isCreating = store.getIsCreating(); - const createError = store.getCreateError(); - ensureScopedStyle("main-registration-view", registrationViewCss); - - const handleCreate = () => { - const data = { - orgName: String( - document.getElementById("org-create-name")?.value || "", - ).trim(), - type: String( - document.getElementById("org-create-type")?.value || "", - ), - }; - - if (!bridge || typeof bridge.requestCreateOrg !== "function") { - store.failCreate("Registration bridge is not available."); - return; - } - - bridge.requestCreateOrg(data); - }; - - return h( - "div", - { className: "split-container", [scopeAttr]: "" }, - h( - "div", - { className: "info-panel" }, - h("h2", null, "Registration Details"), - h( - "p", - null, - "Complete the form to add your organization to the Global Organization Registry.", - ), - h( - "ul", - { className: "create-feature-list" }, - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "Official Organization Designator", - ), - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "Secure Comms Channel", - ), - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "Deployment Roster Access", - ), - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "After-Action Report Tools", - ), - ), - h( - "div", - { className: "price-tag" }, - h("span", { className: "price-label" }, "Registration Fee"), - h("span", { className: "price-value" }, "$50,000"), - ), - ), - h( - "div", - { className: "form-panel card" }, - h("h2", null, "Organization Registration"), - h( - "div", - { className: "app-form" }, - h( - "div", - null, - h("label", null, "Organization Name"), - h("input", { - id: "org-create-name", - type: "text", - placeholder: "e.g. Task Force 141", - }), - ), - h( - "div", - null, - h("label", null, "Organization Type"), - h( - "select", - { id: "org-create-type" }, - h( - "option", - { value: "infantry" }, - "Infantry / Milsim", - ), - h("option", { value: "aviation" }, "Aviation Wing"), - h( - "option", - { value: "pmc" }, - "Private Military Company", - ), - h( - "option", - { value: "support" }, - "Logistics & Support", - ), - ), - ), - h( - "div", - { className: "form-actions" }, - createError - ? h( - "div", - { className: "form-feedback is-error" }, - createError, - ) - : null, - h( - "button", - { - type: "button", - className: "submit-btn", - disabled: isCreating, - onClick: handleCreate, - }, - isCreating - ? "Submitting Registration..." - : "Submit Registration", - ), - h( - "span", - { - className: "cancel-link", - onClick: () => store.setView("home"), - }, - "Cancel / Return to Main", - ), - ), - ), - ), - ); - }; -})(); diff --git a/arma/ui/atm.html b/arma/ui/atm.html deleted file mode 100644 index b6f4444..0000000 --- a/arma/ui/atm.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - ATM - Global Financial Network - - - - - -
- - - diff --git a/arma/ui/atm.js b/arma/ui/atm.js deleted file mode 100644 index 6286ccb..0000000 --- a/arma/ui/atm.js +++ /dev/null @@ -1,407 +0,0 @@ -/** - * ATM App - Vanilla JS Kiosk Implementation - */ - -// --- 1. The "Library" Logic (Reused) --- - -function h(tag, props = {}, ...children) { - const el = document.createElement(tag); - if (props) { - Object.entries(props).forEach(([key, value]) => { - if (key.startsWith("on") && typeof value === "function") { - el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === "className") { - el.className = value; - } else if (key === "style" && typeof value === "object") { - Object.assign(el.style, value); - } else { - el.setAttribute(key, value); - } - }); - } - children.forEach((child) => { - if (typeof child === "string" || typeof child === "number") { - el.appendChild(document.createTextNode(child)); - } else if (child instanceof Node) { - el.appendChild(child); - } else if (Array.isArray(child)) { - child.forEach((c) => el.appendChild(c)); - } - }); - return el; -} - -let _rootContainer = null; -let _rootComponent = null; - -function render(component, container) { - _rootContainer = container; - _rootComponent = component; - _render(); -} - -function _render() { - _rootContainer.innerHTML = ""; - _rootContainer.appendChild(_rootComponent()); -} - -const createSignal = (initialValue) => { - let _val = initialValue; - const getValue = () => _val; - const setValue = (newValue) => { - _val = typeof newValue === "function" ? newValue(_val) : newValue; - _render(); - }; - return [getValue, setValue]; -}; - -// --- 2. ATM Application Components --- - -// Global State -const [getView, setView] = createSignal("pin"); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance' -const [getPin, setPin] = createSignal(""); -const [getCustomAmount, setCustomAmount] = createSignal(""); // For custom withdrawal -const [getBalance, setBalance] = createSignal(1250000); // Shared mockup balance -const [getMessage, setMessage] = createSignal(""); // For feedback - -// Header -function Header() { - return h( - "div", - { className: "header", style: { marginBottom: "2rem" } }, - h("h1", null, "ATM TERMINAL"), - h("p", null, "Global Financial Network"), - ); -} - -// PIN Entry View -function PinView() { - const currentPin = getPin(); - - const handleNumClick = (num) => { - if (currentPin.length < 4) { - setPin((prev) => prev + num); - } - }; - - const handleClear = () => setPin(""); - const handleEnter = () => { - if (currentPin.length === 4) { - // Mock auth success - setView("menu"); - } else { - setMessage("Invalid PIN Length"); - setTimeout(() => setMessage(""), 2000); - } - }; - - return h( - "div", - { className: "card", style: { padding: "3rem 2rem" } }, - h("h2", null, "Enter Security PIN"), - h( - "div", - { className: "pin-display" }, - currentPin.replace(/./g, "•") || "----", - ), - h("p", { style: { color: "red", height: "1.5rem" } }, getMessage()), - h( - "div", - { className: "numpad" }, - ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((num) => - h("button", { onClick: () => handleNumClick(num) }, num), - ), - h( - "button", - { - style: { background: "#ef4444", color: "white" }, - onClick: handleClear, - }, - "C", - ), - h("button", { onClick: () => handleNumClick("0") }, "0"), - h( - "button", - { - style: { background: "#10b981", color: "white" }, - onClick: handleEnter, - }, - "↵", - ), - ), - ); -} - -// Main Menu View -function MenuView() { - return h( - "div", - { className: "kiosk-content" }, - h( - "h2", - { style: { textAlign: "center", marginBottom: "1rem" } }, - "Select Transaction", - ), - h( - "div", - { className: "kiosk-menu-stack" }, - h( - "button", - { className: "kiosk-btn", onClick: () => setView("withdraw") }, - "Withdraw Cash", - ), - h( - "button", - { className: "kiosk-btn", onClick: () => setView("balance") }, - "Check Balance", - ), - h( - "button", - { - className: "kiosk-btn", - style: { - background: "var(--bg-surface)", - color: "var(--text-main)", - border: "1px solid var(--border)", - }, - onClick: () => { - setPin(""); - setView("pin"); - }, - }, - "Cancel Transaction", - ), - ), - ); -} - -// Withdraw View -function WithdrawView() { - const handleWithdraw = (amount) => { - if (getBalance() >= amount) { - setBalance((prev) => prev - amount); - setMessage(`Please take your cash: $${amount}`); - setTimeout(() => { - setMessage(""); - setView("menu"); - }, 3000); - } else { - setMessage("Insufficient Funds"); - setTimeout(() => setMessage(""), 2000); - } - }; - - if (getMessage()) { - return h( - "div", - { - className: "card", - style: { padding: "4rem", textAlign: "center" }, - }, - h("h2", { style: { color: "var(--primary)" } }, getMessage()), - ); - } - - return h( - "div", - { className: "kiosk-content" }, - h( - "h2", - { style: { textAlign: "center", marginBottom: "1rem" } }, - "Select Amount", - ), - h( - "div", - { className: "kiosk-grid" }, - h( - "button", - { className: "kiosk-btn", onClick: () => handleWithdraw(20) }, - "$20", - ), - h( - "button", - { className: "kiosk-btn", onClick: () => handleWithdraw(50) }, - "$50", - ), - h( - "button", - { className: "kiosk-btn", onClick: () => handleWithdraw(100) }, - "$100", - ), - h( - "button", - { - className: "kiosk-btn", - onClick: () => { - setCustomAmount(""); - setView("custom_withdraw"); - }, - }, - "Other Amount", - ), - h( - "button", - { - className: "kiosk-btn", - style: { - gridColumn: "span 2", - background: "var(--text-muted)", - }, - onClick: () => setView("menu"), - }, - "Cancel", - ), - ), - ); -} - -// Custom Withdraw View -function CustomWithdrawView() { - const currentAmount = getCustomAmount(); - - const handleNumClick = (num) => { - if (currentAmount.length < 5) { - // Limit to 5 digits for safety - setCustomAmount((prev) => prev + num); - } - }; - - const handleClear = () => setCustomAmount(""); - - const handleEnter = () => { - const amount = parseInt(currentAmount, 10); - if (amount > 0) { - if (getBalance() >= amount) { - setBalance((prev) => prev - amount); - setMessage(`Please take your cash: $${amount}`); - setTimeout(() => { - setMessage(""); - setView("menu"); - }, 3000); - } else { - setMessage("Insufficient Funds"); - setTimeout(() => setMessage(""), 2000); - } - } else { - setMessage("Invalid Amount"); - setTimeout(() => setMessage(""), 2000); - } - }; - - if (getMessage()) { - return h( - "div", - { - className: "card", - style: { padding: "4rem", textAlign: "center" }, - }, - h("h2", { style: { color: "var(--primary)" } }, getMessage()), - ); - } - - return h( - "div", - { className: "card", style: { padding: "3rem 2rem" } }, - h("h2", null, "Enter Amount"), - h( - "div", - { className: "pin-display" }, - currentAmount ? `$${currentAmount}` : "$0", - ), - h( - "div", - { className: "numpad" }, - ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((num) => - h("button", { onClick: () => handleNumClick(num) }, num), - ), - h( - "button", - { - style: { background: "#ef4444", color: "white" }, - onClick: handleClear, - }, - "C", - ), - h("button", { onClick: () => handleNumClick("0") }, "0"), - h( - "button", - { - style: { background: "#10b981", color: "white" }, - onClick: handleEnter, - }, - "↵", - ), - ), - h( - "button", - { - style: { - width: "100%", - marginTop: "2rem", - padding: "1rem", - background: "var(--text-muted)", - }, - onClick: () => setView("withdraw"), - }, - "Cancel", - ), - ); -} - -// Balance View -function BalanceView() { - return h( - "div", - { className: "card", style: { textAlign: "center", padding: "3rem" } }, - h("h2", { style: { color: "var(--text-muted)" } }, "Available Balance"), - h( - "div", - { - style: { - fontSize: "4rem", - fontWeight: "800", - margin: "2rem 0", - color: "var(--primary-hover)", - }, - }, - "$" + getBalance().toLocaleString(), - ), - h( - "button", - { - className: "kiosk-btn", - style: { width: "100%", maxWidth: "300px", margin: "0 auto" }, - onClick: () => setView("menu"), - }, - "Return to Menu", - ), - ); -} - -// Main App -function App() { - const view = getView(); - - let mainContent; - if (view === "pin") { - mainContent = PinView(); - } else if (view === "menu") { - mainContent = MenuView(); - } else if (view === "withdraw") { - mainContent = WithdrawView(); - } else if (view === "custom_withdraw") { - mainContent = CustomWithdrawView(); - } else if (view === "balance") { - mainContent = BalanceView(); - } - - return h( - "main", - null, - h("div", { className: "container" }, Header(), mainContent), - ); -} - -// Mount -const root = document.getElementById("app"); -render(App, root); diff --git a/arma/ui/bank.html b/arma/ui/bank.html deleted file mode 100644 index 64c2005..0000000 --- a/arma/ui/bank.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - FDIC - Global Financial Network - - - - -
- - - diff --git a/arma/ui/bank.js b/arma/ui/bank.js deleted file mode 100644 index 75038cf..0000000 --- a/arma/ui/bank.js +++ /dev/null @@ -1,441 +0,0 @@ -/** - * Player Bank App - Vanilla JS "React-like" Implementation - */ - -// --- 1. The "Library" Logic (Reused) --- - -function h(tag, props = {}, ...children) { - const el = document.createElement(tag); - if (props) { - Object.entries(props).forEach(([key, value]) => { - if (key.startsWith("on") && typeof value === "function") { - el.addEventListener(key.substring(2).toLowerCase(), value); - } else if (key === "className") { - el.className = value; - } else if (key === "style" && typeof value === "object") { - Object.assign(el.style, value); - } else { - el.setAttribute(key, value); - } - }); - } - children.forEach((child) => { - if (typeof child === "string" || typeof child === "number") { - el.appendChild(document.createTextNode(child)); - } else if (child instanceof Node) { - el.appendChild(child); - } else if (Array.isArray(child)) { - child.forEach((c) => el.appendChild(c)); - } - }); - return el; -} - -let _rootContainer = null; -let _rootComponent = null; - -function render(component, container) { - _rootContainer = container; - _rootComponent = component; - _render(); -} - -function _render() { - _rootContainer.innerHTML = ""; - _rootContainer.appendChild(_rootComponent()); -} - -const createSignal = (initialValue) => { - let _val = initialValue; - const getValue = () => _val; - const setValue = (newValue) => { - _val = typeof newValue === "function" ? newValue(_val) : newValue; - _render(); - }; - return [getValue, setValue]; -}; - -// --- 2. Bank Application Components --- - -// Global State -const [getView, setView] = createSignal("login"); // 'login', 'dashboard' -const [getBalance, setBalance] = createSignal(1250000); -const [getPending, setPending] = createSignal(45250); // Mock pending earnings -const [getTransactions, setTransactions] = createSignal([ - { - id: 1, - type: "credit", - desc: "Contract Payment: OP-442", - amount: 150000, - date: "2026-02-05", - }, - { - id: 2, - type: "debit", - desc: "Equipment Purchase: Ammunition", - amount: -4500, - date: "2026-02-04", - }, - { - id: 3, - type: "debit", - desc: "Vehicle Maintenance", - amount: -1200, - date: "2026-02-03", - }, -]); - -// Header -function Header() { - return h( - "div", - { className: "header" }, - h( - "h1", - { - style: { cursor: "pointer" }, - onClick: () => setView("login"), - }, - "Global Financial Network", - ), - h("p", null, "Secure Banking"), - ); -} - -// Login View -function BankLogin() { - const handleSubmit = (e) => { - e.preventDefault(); - setView("dashboard"); - }; - - return h( - "div", - { className: "card", style: { maxWidth: "400px", margin: "0 auto" } }, - h("h2", null, "Secure Access"), - h( - "form", - { onSubmit: handleSubmit }, - h( - "div", - null, - h("label", null, "Account ID"), - h("input", { type: "text", placeholder: "xxxx-xxxx-xxxx" }), - ), - h( - "div", - null, - h("label", null, "Security PIN"), - h("input", { type: "password", placeholder: "••••" }), - ), - h( - "div", - { className: "form-actions" }, - h( - "button", - { type: "submit", style: { width: "100%" } }, - "Authenticate", - ), - h( - "p", - { - style: { - fontSize: "0.8rem", - color: "var(--text-muted)", - marginTop: "1rem", - }, - }, - "Authorized Personnel Only", - ), - ), - ), - ); -} - -// Transaction History Helper -function TransactionHistory() { - const transactions = getTransactions(); - - return h( - "div", - { className: "card" }, - h( - "h3", - { - style: { - textAlign: "left", - borderBottom: "1px solid var(--border)", - paddingBottom: "1rem", - marginBottom: "1rem", - }, - }, - "Recent Transactions", - ), - h( - "ul", - { style: { listStyle: "none", padding: 0 } }, - ...transactions.map((tx) => - h( - "li", - { - style: { - display: "flex", - justifyContent: "space-between", - padding: "0.75rem 0", - borderBottom: "1px solid var(--bg-surface-hover)", - }, - }, - h( - "div", - { style: { textAlign: "left" } }, - h("div", { style: { fontWeight: "500" } }, tx.desc), - h( - "div", - { - style: { - fontSize: "0.85rem", - color: "var(--text-muted)", - }, - }, - tx.date, - ), - ), - h( - "div", - { - style: { - fontWeight: "700", - color: - tx.type === "credit" - ? "#10b981" - : "#ef4444", - }, - }, - (tx.type === "credit" ? "+" : "") + - "$" + - Math.abs(tx.amount).toLocaleString(), - ), - ), - ), - ), - ); -} - -// Transfer Form -function TransferForm() { - const handleSubmit = (e) => { - e.preventDefault(); - const formData = new FormData(e.target); - const amount = parseFloat(formData.get("amount")); - - if (amount > 0 && amount <= getBalance()) { - setBalance((prev) => prev - amount); - const newTx = { - id: Date.now(), - type: "debit", - desc: "Transfer to " + formData.get("recipient"), - amount: -amount, - date: new Date().toISOString().split("T")[0], - }; - setTransactions((prev) => [newTx, ...prev]); - } - }; - - return h( - "div", - { className: "card" }, - h("h2", null, "Wire Transfer"), - h( - "form", - { onSubmit: handleSubmit }, - h( - "div", - null, - h("label", null, "Recipient Name / GUID"), - h("input", { - name: "recipient", - type: "text", - placeholder: "Enter Name or GUID", - }), - ), - h( - "div", - null, - h("label", null, "Amount"), - h("input", { - name: "amount", - type: "number", - placeholder: "0.00", - }), - ), - h("button", { type: "submit" }, "Send Funds"), - ), - ); -} - -// Dashboard View -function BankDashboard() { - return h( - "div", - { className: "content" }, - // Top Row: Balance - h( - "div", - { className: "card", style: { gridColumn: "span 2" } }, - h( - "h2", - { - style: { - fontSize: "1.2rem", - color: "var(--text-muted)", - textTransform: "uppercase", - letterSpacing: "0.05em", - }, - }, - "Total Balance", - ), - h( - "div", - { - style: { - fontSize: "2.8rem", - fontWeight: "800", - color: "var(--primary-hover)", - margin: "1rem 0", - }, - }, - "$" + getBalance().toLocaleString(), - ), - h( - "div", - { - style: { - textAlign: "center", - marginBottom: "1.5rem", - color: "var(--text-muted)", - fontSize: "1.2rem", - }, - }, - "Pending: ", - h( - "span", - { style: { color: "#fbbf24", fontWeight: "bold" } }, - "$" + getPending().toLocaleString(), - ), - ), - h( - "div", - { - style: { - display: "flex", - gap: "1rem", - justifyContent: "center", - }, - }, - h( - "button", - { - onClick: () => { - const pending = getPending(); - if (pending > 0) { - setBalance((prev) => prev + pending); - setPending(0); - const newTx = { - id: Date.now(), - type: "credit", - desc: "Field Deposit", - amount: pending, - date: new Date() - .toISOString() - .split("T")[0], - }; - setTransactions((prev) => [newTx, ...prev]); - } - }, - style: { - opacity: getPending() > 0 ? "1" : "0.5", - cursor: getPending() > 0 ? "pointer" : "default", - }, - }, - "Deposit Pending", - ), - h( - "button", - { - style: { - background: "var(--bg-surface-hover)", - color: "var(--text-main)", - border: "1px solid var(--border)", - }, - }, - "Statement", - ), - ), - ), - // Middle Row: Transfer Form - TransferForm(), - // Bottom Row: History (Full Width in simplified grid, or separate) - TransactionHistory(), - ); -} - -// Footer -function Footer() { - return h( - "div", - { className: "footer" }, - h( - "div", - { className: "wrapper" }, - h( - "div", - null, - h("h3", null, "Secure Banking"), - h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - h("li", null, "FDIC Insured"), - h("li", null, "Fraud Protection"), - h("li", null, "24/7 Support"), - h("li", null, "API Access"), - ), - ), - h( - "div", - null, - h("h3", null, "Notices"), - h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - h("li", null, "Terms of Service"), - h("li", null, "Privacy Policy"), - h("li", null, "Interest Rates"), - h("li", null, "Report Fraud"), - ), - ), - ), - ); -} - -// Main App -function App() { - const view = getView(); - - let mainContent; - if (view === "login") { - mainContent = BankLogin(); - } else if (view === "dashboard") { - mainContent = BankDashboard(); - } - - return h( - "main", - null, - h("div", { className: "container" }, Header(), mainContent), - Footer(), - ); -} - -// Mount -const root = document.getElementById("app"); -render(App, root); diff --git a/arma/ui/style.css b/arma/ui/style.css deleted file mode 100644 index f79f5ae..0000000 --- a/arma/ui/style.css +++ /dev/null @@ -1,306 +0,0 @@ -:root { - --bg-app: #fdfcf8; - /* Warm white */ - --bg-surface: #ffffff; - --bg-surface-hover: #f1f5f9; - --primary: #475569; - /* Slate gray */ - --primary-hover: #1e293b; - --text-main: #1f2937; - --text-muted: #64748b; - --text-inverse: #f8fafc; - --border: #e2e8f0; - --radius: 8px; - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --footer-bg: #1e293b; - /* Dark slate for footer */ -} - -body { - font-family: - "Inter", - system-ui, - -apple-system, - sans-serif; - margin: 0; - padding: 0; - background: var(--bg-app); - color: var(--text-main); - line-height: 1.6; -} - -#app { - min-height: 100vh; -} - -main { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.container { - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 2rem; - flex: 1; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -/* Header */ -.header { - text-align: center; - margin-bottom: 3rem; - padding-bottom: 2rem; - border-bottom: 1px solid var(--border); - - h1 { - font-size: 2.5rem; - font-weight: 700; - margin-bottom: 0.5rem; - letter-spacing: -0.025em; - color: var(--primary-hover); - } - - p { - color: var(--text-muted); - font-size: 1.1rem; - } -} - -.content { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin-bottom: 2rem; -} - -/* Cards */ -.card { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 2rem; - box-shadow: var(--shadow); - text-align: center; - - h2 { - margin-top: 0; - font-size: 1.8rem; - color: var(--primary-hover); - } -} - -/* Buttons */ -button { - background: var(--primary); - color: white; - border: none; - padding: 0.75rem 1.5rem; - border-radius: var(--radius); - cursor: pointer; - font-size: 1rem; - font-weight: 500; - transition: all 0.2s ease; - - &:hover { - background: var(--primary-hover); - transform: translateY(-1px); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); - } - - & + & { - margin-left: 1rem; - } -} - -/* Split Layout */ -.split-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: center; - width: 100%; -} - -.info-panel { - text-align: left; - padding: 1rem; -} - -/* Forms */ -form { - display: flex; - flex-direction: column; - gap: 1rem; - text-align: left; - - label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; - } - - input, - select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: border-color 0.2s; - - &:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); - } - } - - .form-actions { - margin-top: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; - } - - .cancel-link { - font-size: 0.9rem; - color: var(--text-muted); - cursor: pointer; - text-decoration: underline; - - &:hover { - color: var(--primary); - } - } -} - -/* Footer */ -.footer { - margin-top: auto; - background: var(--footer-bg); - color: var(--text-inverse); - display: block; - - .wrapper { - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 3rem 2rem; - box-sizing: border-box; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 4rem; - } - - h3 { - color: var(--text-inverse); - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.1em; - font-weight: 700; - margin-bottom: 1.5rem; - border-bottom: 1px solid #475569 !important; - padding-bottom: 0.5rem; - margin-right: 1rem; - } - - ul { - li { - color: #cbd5e1; - font-size: 0.95rem; - margin-bottom: 0.75rem !important; - cursor: pointer; - transition: color 0.2s; - - &:hover { - color: white; - } - } - } -} - -/* ATM Kiosk Styles */ -.kiosk-content { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -} - -.kiosk-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1.5rem; - margin-top: 2rem; - width: 100%; - max-width: 600px; - /* Constrain width for better look */ -} - -.kiosk-menu-stack { - display: flex; - flex-direction: column; - gap: 1.5rem; - margin-top: 2rem; - width: 100%; - max-width: 600px; - /* Narrower for vertical stack */ -} - -.kiosk-btn { - padding: 2rem; - font-size: 1.25rem; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.5rem; - height: 100%; - min-height: 120px; - margin: 0; -} - -.numpad { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1rem; - max-width: 300px; - margin: 0 auto; - - button { - padding: 1.5rem; - font-size: 1.5rem; - background: var(--bg-surface); - color: var(--text-main); - border: 1px solid var(--border); - box-shadow: var(--shadow); - margin: 0; - - &:hover { - background: var(--primary); - color: white; - border-color: var(--primary); - } - } -} - -.pin-display { - font-size: 2.5rem; - letter-spacing: 0.5rem; - text-align: center; - margin-bottom: 2rem; - font-family: monospace; - color: var(--primary); -}