Implement org credit line debt and bank repayment flow (#2)
## Summary This finishes the org credit line workflow so it behaves like reserved treasury-backed credit instead of a simple member allowance. ## What changed - reserve org funds immediately when a credit line is assigned - track credit lines with: - approved amount - available amount - outstanding principal - interest rate - amount due - consume reserved credit during store checkout without charging org funds a second time - add credit line repayment through the bank app - sync richer credit line state into org and bank payloads/UI - keep legacy `amount` compatibility mapped to available credit for older consumers ## User-facing behavior - assigning a credit line now reduces available org funds immediately - spending on `credit_line` reduces available credit and creates debt with interest - the bank app now shows outstanding credit debt and allows repayment from personal bank funds - the org treasury view now shows reserved credit and outstanding due totals ## Validation - `cargo fmt` - `npm run build:webui` - `cargo test -p forge-services --quiet` - `cargo test -p forge-server --quiet` ## Follow-up checks - validate in-game that assigning a credit line reduces org funds immediately - validate store checkout with `credit_line` updates available credit and debt correctly - validate bank repayment decreases player bank balance, increases org funds, and reduces amount due Co-authored-by: Jacob Schmidt <innovativestudios@outlook.com> Reviewed-on: #2
This commit is contained in:
parent
7a8ca6b237
commit
ff7ff0c4e5
@ -1,3 +1,3 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initActorClass);
|
||||
PREP(initRepository);
|
||||
PREP(openUI);
|
||||
|
||||
@ -23,17 +23,17 @@ player addEventHandler ["Respawn", {
|
||||
[SRPC(economy,onRespawn), [_unit, _corpse, _uid]] call CFUNC(serverEvent);
|
||||
}];
|
||||
|
||||
if (isNil QGVAR(ActorClass)) then { call FUNC(initActorClass); };
|
||||
if (isNil QGVAR(ActorRepository)) then { call FUNC(initRepository); };
|
||||
|
||||
[QGVAR(initActor), {
|
||||
GVAR(ActorClass) call ["init", []];
|
||||
GVAR(ActorRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(onActorRespawn), {
|
||||
params [["_loadout", [], [[]]], ["_medSpawnPos", [0,0,0], [[]]], ["_medSpawnDir", 0, [0]]];
|
||||
|
||||
private _message = ["warning", "Medical Alert", "You have been revived at a medical facility.", 5000];
|
||||
EGVAR(notifications,NotificationClass) call ["create", _message];
|
||||
EGVAR(notifications,NotificationService) call ["create", _message];
|
||||
|
||||
player setUnitLoadout _loadout;
|
||||
player setPosATL _medSpawnPos;
|
||||
@ -53,14 +53,14 @@ if (isNil QGVAR(ActorClass)) then { call FUNC(initActorClass); };
|
||||
[QGVAR(responseInitActor), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(ActorClass) call ["sync", [_data, true]];
|
||||
GVAR(ActorRepository) call ["sync", [_data, true]];
|
||||
cutText ["", "PLAIN", 1];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncActor), {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(ActorClass) call ["sync", [_data, _jip]];
|
||||
GVAR(ActorRepository) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(initActor), []] call CFUNC(localEvent);
|
||||
@ -68,6 +68,6 @@ if (isNil QGVAR(ActorClass)) then { call FUNC(initActorClass); };
|
||||
[{
|
||||
GETVAR(player,FORGE_isLoaded,false)
|
||||
}, {
|
||||
private _holster = GVAR(ActorClass) call ["get", ["holster", true]];
|
||||
private _holster = GVAR(ActorRepository) call ["get", ["holster", true]];
|
||||
if (_holster) then { [player] call AFUNC(weaponselect,putWeaponAway); };
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_handleUIEvents.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-02-17
|
||||
* Last Update: 2026-03-28
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
@ -31,10 +31,11 @@ private _data = _alert get "data";
|
||||
diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _event, _data];
|
||||
|
||||
switch (_event) do {
|
||||
case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; };
|
||||
case "actor::get::actions": { GVAR(ActorRepository) call ["getNearbyActions", [_control]]; };
|
||||
case "actor::close::menu": { closeDialog 1; };
|
||||
case "actor::open::atm": { [true] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::cad": { [] spawn EFUNC(cad,openUI); };
|
||||
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
|
||||
case "actor::open::garage": { [] spawn EFUNC(garage,openUI); };
|
||||
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
|
||||
|
||||
@ -1,29 +1,27 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initActorClass.sqf
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-02-17
|
||||
* Public: Yes
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the actor class for managing player data.
|
||||
* Provides methods for saving, loading, and applying actor data.
|
||||
* Initializes the actor repository for managing player actor data.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Actor class object [HASHMAP OBJECT]
|
||||
* Actor repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_actor_fnc_initActorClass
|
||||
* call forge_client_actor_fnc_initRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "ActorBaseClass"],
|
||||
GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "ActorRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["actor", createHashMap];
|
||||
@ -33,9 +31,10 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
|
||||
["init", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(actor,requestInitActor), [_uid]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
systemChat format ["Actor loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Actor] Actor Class Initialized!";
|
||||
systemChat format ["Actor loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:Actor] Actor Repository Initialized!";
|
||||
}],
|
||||
["save", compileFinal {
|
||||
params [["_sync", false, [false]]];
|
||||
@ -68,29 +67,23 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
_self set ["actor", _actor];
|
||||
SETPVAR(player,FORGE_isLoaded,true);
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Actor] Sync completed";
|
||||
}],
|
||||
["get", compileFinal {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
private _actor = _self get "actor";
|
||||
_actor getOrDefault [_key, _default];
|
||||
}],
|
||||
["applyPosition", compileFinal {
|
||||
private _position = _self call ["get", ["position", [0, 0, 0]]];
|
||||
|
||||
if (GVAR(enableLoc)) then {
|
||||
player setPosASL _position;
|
||||
|
||||
private _pAlt = ((getPosATLVisual player) select 2);
|
||||
private _pVelZ = ((velocity player) select 2);
|
||||
|
||||
if (_pAlt > 5 && _pVelZ < 0) then {
|
||||
player setVelocity [0, 0, 0];
|
||||
player setPosATL [((getPosATLVisual player) select 0), ((getPosATLVisual player) select 1), 1];
|
||||
|
||||
hint "You logged off mid air. You were moved to a safe position on the ground";
|
||||
};
|
||||
};
|
||||
@ -113,9 +106,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
|
||||
}],
|
||||
["getNearbyActions", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]]];
|
||||
|
||||
private _nearbyActions = [];
|
||||
|
||||
{
|
||||
private _storeType = _x getVariable ["storeType", ""];
|
||||
private _isAtm = _x getVariable ["isAtm", false];
|
||||
@ -140,5 +131,5 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(ActorClass) = createHashMapObject [GVAR(ActorBaseClass)];
|
||||
GVAR(ActorClass)
|
||||
GVAR(ActorRepository) = createHashMapObject [GVAR(ActorRepositoryBaseClass)];
|
||||
GVAR(ActorRepository)
|
||||
@ -100,6 +100,12 @@ const actions = {
|
||||
//=============================================================================
|
||||
|
||||
const baseMenuItems = [
|
||||
{
|
||||
id: "cad",
|
||||
title: "CAD",
|
||||
description: "Access CAD (Computer Aided Dispatch)",
|
||||
action: "actor::open::cad",
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
title: "Phone",
|
||||
@ -133,6 +139,12 @@ const actionDefinitions = {
|
||||
description: "Access your bank account and manage finances",
|
||||
action: "actor::open::bank",
|
||||
},
|
||||
cad: {
|
||||
id: "cad",
|
||||
title: "CAD",
|
||||
description: "Access the CAD",
|
||||
action: "actor::open::cad",
|
||||
},
|
||||
phone: {
|
||||
id: "phone",
|
||||
title: "Phone",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initClass);
|
||||
PREP(initSessionService);
|
||||
PREP(initRepository);
|
||||
PREP(initUIBridge);
|
||||
PREP(openUI);
|
||||
|
||||
@ -1,33 +1,48 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(BankClass)) then { call FUNC(initClass); };
|
||||
if (isNil QGVAR(BankSessionService)) then { call FUNC(initSessionService); };
|
||||
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(initBank), {
|
||||
GVAR(BankClass) call ["init", []];
|
||||
GVAR(BankRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitBank), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(BankClass) call ["sync", [_data, true]];
|
||||
GVAR(BankRepository) call ["markLoaded", []];
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["refreshSession", []];
|
||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncBank), {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(BankClass) call ["sync", [_data, _jip]];
|
||||
GVAR(BankRepository) call ["markLoaded", []];
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["refreshSession", []];
|
||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseHydrateBank), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseBankNotice), {
|
||||
params [["_type", "error", [""]], ["_message", "", [""]]];
|
||||
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(org,OrgClass) get "isLoaded";
|
||||
EGVAR(actor,ActorRepository) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initBank), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
|
||||
@ -68,6 +68,16 @@ switch (_event) do {
|
||||
GVAR(BankUIBridge) call ["handleDepositEarningsRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::repayCreditLine::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleRepayCreditLineRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::pin::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
|
||||
};
|
||||
};
|
||||
default {
|
||||
hint format ["Unhandled bank UI event: %1", _event];
|
||||
};
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the bank class for account sync and access helpers.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(BankBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "BankBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["account", createHashMapFromArray [
|
||||
["bank", 0],
|
||||
["cash", 0],
|
||||
["earnings", 0],
|
||||
["pin", 1234],
|
||||
["transactions", []]
|
||||
]];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["getAccountState", compileFinal {
|
||||
_self getOrDefault ["account", createHashMap]
|
||||
}],
|
||||
["get", compileFinal {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
private _account = _self getOrDefault ["account", createHashMap];
|
||||
_account getOrDefault [_key, _default]
|
||||
}],
|
||||
["init", compileFinal {
|
||||
[SRPC(bank,requestInitBank), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["save", compileFinal {
|
||||
[SRPC(bank,requestSaveBank), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
private _account = _self getOrDefault ["account", createHashMap];
|
||||
{
|
||||
_account set [_x, _y];
|
||||
} forEach _data;
|
||||
|
||||
_self set ["account", _account];
|
||||
if !(_self getOrDefault ["isLoaded", false]) then {
|
||||
_self set ["isLoaded", true];
|
||||
};
|
||||
|
||||
true
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(BankClass) = createHashMapObject [GVAR(BankBaseClass)];
|
||||
GVAR(BankClass)
|
||||
44
arma/client/addons/bank/functions/fnc_initRepository.sqf
Normal file
44
arma/client/addons/bank/functions/fnc_initRepository.sqf
Normal file
@ -0,0 +1,44 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the bank repository for client bank lifecycle state.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Bank repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_bank_fnc_initRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(BankRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "BankRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", compileFinal {
|
||||
[SRPC(bank,requestInitBank), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
systemChat format ["Bank loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:Bank] Bank Repository Initialized!";
|
||||
}],
|
||||
["markLoaded", compileFinal {
|
||||
if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; };
|
||||
true
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(BankRepository) = createHashMapObject [GVAR(BankRepositoryBaseClass)];
|
||||
GVAR(BankRepository)
|
||||
@ -1,80 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initSessionService.sqf
|
||||
* Author: IDSolutions
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the bank session service that shapes the browser payload.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(BankSessionServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "BankSessionServiceBaseClass"],
|
||||
["buildTransferTargets", compileFinal {
|
||||
private _targets = [];
|
||||
|
||||
{
|
||||
if (isNull _x || { _x isEqualTo player }) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
private _uid = getPlayerUID _x;
|
||||
private _name = name _x;
|
||||
if (_uid isEqualTo "" || { _name isEqualTo "" }) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
_targets pushBack (createHashMapFromArray [
|
||||
["name", _name],
|
||||
["uid", _uid]
|
||||
]);
|
||||
} forEach allPlayers;
|
||||
|
||||
private _targetPairs = _targets apply {
|
||||
[toLowerANSI (_x getOrDefault ["name", ""]), _x]
|
||||
};
|
||||
_targetPairs sort true;
|
||||
_targetPairs apply {
|
||||
_x param [1, createHashMap]
|
||||
}
|
||||
}],
|
||||
["buildPayload", compileFinal {
|
||||
params [["_mode", "bank", [""]]];
|
||||
|
||||
private _account = if (isNil QGVAR(BankClass)) then {
|
||||
createHashMap
|
||||
} else {
|
||||
GVAR(BankClass) call ["getAccountState", []]
|
||||
};
|
||||
|
||||
private _orgFunds = 0;
|
||||
private _orgName = "";
|
||||
if !(isNil QEGVAR(org,OrgClass)) then {
|
||||
_orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]];
|
||||
_orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]];
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["session", createHashMapFromArray [
|
||||
["mode", ["bank", "atm"] select (toLowerANSI _mode isEqualTo "atm")],
|
||||
["orgFunds", _orgFunds],
|
||||
["orgName", _orgName],
|
||||
["playerName", name player],
|
||||
["transferTargets", _self call ["buildTransferTargets", []]],
|
||||
["uid", getPlayerUID player]
|
||||
]],
|
||||
["account", createHashMapFromArray [
|
||||
["bank", _account getOrDefault ["bank", 0]],
|
||||
["cash", _account getOrDefault ["cash", 0]],
|
||||
["earnings", _account getOrDefault ["earnings", 0]],
|
||||
["pin", str (_account getOrDefault ["pin", 1234])],
|
||||
["transactions", _account getOrDefault ["transactions", []]]
|
||||
]]
|
||||
]
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(BankSessionService) = createHashMapObject [GVAR(BankSessionServiceBaseClass)];
|
||||
GVAR(BankSessionService)
|
||||
@ -3,10 +3,20 @@
|
||||
/*
|
||||
* File: fnc_initUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the bank web UI bridge.
|
||||
* Initializes the bank UI bridge for browser control state and bank UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Bank UI bridge object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_bank_fnc_initUIBridge;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
@ -19,9 +29,6 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#create", compileFinal {
|
||||
_self set ["mode", "bank"];
|
||||
}],
|
||||
["buildPayload", compileFinal {
|
||||
GVAR(BankSessionService) call ["buildPayload", [_self call ["getMode", []]]]
|
||||
}],
|
||||
["getActiveBrowserControl", compileFinal {
|
||||
private _display = uiNamespace getVariable ["RscBank", displayNull];
|
||||
if (isNull _display) exitWith {
|
||||
@ -36,14 +43,16 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
["getMode", compileFinal {
|
||||
_self getOrDefault ["mode", "bank"]
|
||||
}],
|
||||
["hasOpenScreen", compileFinal {
|
||||
private _screen = _self call ["getScreen", []];
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
|
||||
!(isNull _control) && { _screen call ["isReady", []] }
|
||||
}],
|
||||
["handleDepositEarningsRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
if (_amount <= 0) exitWith {
|
||||
_self call ["sendNotice", ["error", "No earnings are available to deposit."]];
|
||||
};
|
||||
|
||||
[SRPC(bank,requestDepositEarnings), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
@ -51,22 +60,53 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
if (_amount <= 0) exitWith {
|
||||
_self call ["sendNotice", ["error", "Enter a valid deposit amount."]];
|
||||
};
|
||||
|
||||
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleRepayCreditLineRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
[SRPC(bank,requestRepayCreditLine), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleHydrateResponse", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_event", "bank::hydrate", [""]]];
|
||||
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", [_event, _data, _self call ["getActiveBrowserControl", []]]]
|
||||
}],
|
||||
["handleAccountSyncResponse", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", ["bank::sync", _data, _self call ["getActiveBrowserControl", []]]]
|
||||
}],
|
||||
["handleNoticeResponse", compileFinal {
|
||||
params [["_type", "error", [""]], ["_message", "", [""]]];
|
||||
|
||||
_self call ["sendNotice", [_type, _message]]
|
||||
}],
|
||||
["handleReady", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _screen = _self call ["getScreen", []];
|
||||
_screen call ["setControl", [_control]];
|
||||
_screen call ["markReady", [true]];
|
||||
|
||||
_self call ["flushPendingEvents", []];
|
||||
_self call ["sendEvent", ["bank::hydrate", _self call ["buildPayload", []], _control]];
|
||||
|
||||
_self call ["requestHydrate", [true]]
|
||||
}],
|
||||
["handleSubmitPinRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _pin = _data getOrDefault ["pin", ""];
|
||||
if !(_pin isEqualType "") then { _pin = str _pin; };
|
||||
|
||||
[SRPC(bank,requestSubmitPin), [getPlayerUID player, _pin]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleTransferRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
@ -75,18 +115,6 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _target = _data getOrDefault ["target", ""];
|
||||
private _from = toLowerANSI (_data getOrDefault ["from", "bank"]);
|
||||
|
||||
if (_target isEqualTo "") exitWith {
|
||||
_self call ["sendNotice", ["error", "Select a transfer recipient."]];
|
||||
};
|
||||
|
||||
if (_target isEqualTo getPlayerUID player) exitWith {
|
||||
_self call ["sendNotice", ["error", "You cannot transfer funds to yourself."]];
|
||||
};
|
||||
|
||||
if (_amount <= 0) exitWith {
|
||||
_self call ["sendNotice", ["error", "Enter a valid transfer amount."]];
|
||||
};
|
||||
|
||||
[SRPC(bank,requestTransfer), [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
@ -94,23 +122,24 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
if (_amount <= 0) exitWith {
|
||||
_self call ["sendNotice", ["error", "Enter a valid withdrawal amount."]];
|
||||
};
|
||||
|
||||
[SRPC(bank,requestWithdraw), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["refreshSession", compileFinal {
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
if (isNull _control) exitWith { false };
|
||||
_self call ["requestHydrate", [false]]
|
||||
}],
|
||||
["requestHydrate", compileFinal {
|
||||
params [["_resetAuthorization", false, [false]]];
|
||||
|
||||
_self call ["sendEvent", ["bank::sync", _self call ["buildPayload", []], _control]]
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
[SRPC(bank,requestHydrateBank), [getPlayerUID player, _self call ["getMode", []], _resetAuthorization]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["sendNotice", compileFinal {
|
||||
params [["_type", "error", [""]], ["_message", "", [""]], ["_control", controlNull, [controlNull]]];
|
||||
|
||||
if (_message isEqualTo "") exitWith { false };
|
||||
if (_message isEqualTo "" || { !(_self call ["hasOpenScreen", []]) }) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", ["bank::notice", createHashMapFromArray [
|
||||
["message", _message],
|
||||
@ -121,9 +150,7 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
params [["_mode", "bank", [""]]];
|
||||
|
||||
private _finalMode = toLowerANSI _mode;
|
||||
if !(_finalMode in ["bank", "atm"]) then {
|
||||
_finalMode = "bank";
|
||||
};
|
||||
if !(_finalMode in ["bank", "atm"]) then { _finalMode = "bank"; };
|
||||
|
||||
_self set ["mode", _finalMode];
|
||||
_finalMode
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -12,9 +12,15 @@
|
||||
store.hydrateFromPayload(payloadData);
|
||||
}
|
||||
|
||||
function syncAccount(payloadData) {
|
||||
BankApp.data.applyAccountPatch(payloadData);
|
||||
store.syncAccountPatch();
|
||||
}
|
||||
|
||||
bridge.on("bank::hydrate", hydrate);
|
||||
bridge.on("bank::sync", hydrate);
|
||||
bridge.on("bank::sync", syncAccount);
|
||||
bridge.on("bank::notice", (payloadData) => {
|
||||
store.finishAction();
|
||||
if (BankApp.actions) {
|
||||
BankApp.actions.showNotice(
|
||||
payloadData.type || "error",
|
||||
@ -37,9 +43,15 @@
|
||||
requestDepositEarnings(payload) {
|
||||
return bridge.send("bank::depositEarnings::request", payload);
|
||||
},
|
||||
requestRepayCreditLine(payload) {
|
||||
return bridge.send("bank::repayCreditLine::request", payload);
|
||||
},
|
||||
requestRefresh() {
|
||||
return bridge.send("bank::refresh", {});
|
||||
},
|
||||
requestSubmitPin(payload) {
|
||||
return bridge.send("bank::pin::request", payload);
|
||||
},
|
||||
requestTransfer(payload) {
|
||||
return bridge.send("bank::transfer::request", payload);
|
||||
},
|
||||
|
||||
@ -2,6 +2,14 @@
|
||||
const BankApp = (window.BankApp = window.BankApp || {});
|
||||
|
||||
const defaultSession = {
|
||||
atmAuthorized: false,
|
||||
creditLine: {
|
||||
amountDue: 0,
|
||||
approvedAmount: 0,
|
||||
availableAmount: 0,
|
||||
interestRate: 0.1,
|
||||
outstandingPrincipal: 0,
|
||||
},
|
||||
mode: "bank",
|
||||
orgFunds: 0,
|
||||
orgName: "",
|
||||
@ -14,7 +22,6 @@
|
||||
bank: 0,
|
||||
cash: 0,
|
||||
earnings: 0,
|
||||
pin: "1234",
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
@ -30,6 +37,13 @@
|
||||
BankApp.data = {
|
||||
account: Object.assign({}, defaultAccount),
|
||||
session: Object.assign({}, defaultSession),
|
||||
applyAccountPatch(patch) {
|
||||
const nextAccount = Object.assign({}, this.account, patch || {});
|
||||
replaceObject(
|
||||
this.account,
|
||||
Object.assign({}, defaultAccount, nextAccount),
|
||||
);
|
||||
},
|
||||
applyHydratePayload(payload) {
|
||||
replaceObject(
|
||||
this.session,
|
||||
|
||||
@ -89,6 +89,16 @@
|
||||
"Reference value pulled from the organization treasury.",
|
||||
session.orgFunds > 0 ? "success" : "",
|
||||
),
|
||||
metricCard(
|
||||
"Credit Due",
|
||||
formatCurrency(session.creditLine?.amountDue || 0),
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? `Outstanding principal ${formatCurrency(session.creditLine?.outstandingPrincipal || 0)} at ${Math.round(Number(session.creditLine?.interestRate || 0) * 100)}% interest.`
|
||||
: "No active credit repayment is currently due.",
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? "warning"
|
||||
: "",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -238,6 +248,63 @@
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Credit"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Repay Org Credit",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-form-stack" },
|
||||
h(
|
||||
"p",
|
||||
{ className: "bank-card-copy" },
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? `Outstanding due ${formatCurrency(session.creditLine.amountDue || 0)}. Available reserved credit ${formatCurrency(session.creditLine.availableAmount || 0)}.`
|
||||
: "No repayment is currently due on the assigned organization credit line.",
|
||||
),
|
||||
h("input", {
|
||||
id: "bank-credit-line-amount",
|
||||
className: "bank-input",
|
||||
type: "number",
|
||||
min: "1",
|
||||
placeholder: "Enter repayment amount",
|
||||
}),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled:
|
||||
pending("repaycreditline") ||
|
||||
Number(session.creditLine?.amountDue || 0) <= 0,
|
||||
onClick: () => {
|
||||
const sent = actions.requestRepayCreditLine(
|
||||
readInputValue("bank-credit-line-amount"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-credit-line-amount");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("repaycreditline")
|
||||
? "Posting Repayment..."
|
||||
: "Repay Credit Line",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -4,14 +4,6 @@
|
||||
|
||||
let noticeTimer = null;
|
||||
|
||||
function getAccount() {
|
||||
return BankApp.data?.account || {};
|
||||
}
|
||||
|
||||
function getSession() {
|
||||
return BankApp.data?.session || {};
|
||||
}
|
||||
|
||||
function normalizeAmount(value) {
|
||||
const amount = Math.floor(Number(value || 0));
|
||||
return Number.isFinite(amount) ? amount : 0;
|
||||
@ -58,18 +50,6 @@
|
||||
|
||||
function requestDeposit(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const account = getAccount();
|
||||
|
||||
if (amount <= 0) {
|
||||
showNotice("error", "Enter a valid deposit amount.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount > Number(account.cash || 0)) {
|
||||
showNotice("error", "Cash on hand cannot cover that deposit.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestDeposit !== "function") {
|
||||
showNotice("error", "Deposit bridge is unavailable.");
|
||||
@ -89,18 +69,6 @@
|
||||
|
||||
function requestWithdraw(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const account = getAccount();
|
||||
|
||||
if (amount <= 0) {
|
||||
showNotice("error", "Enter a valid withdrawal amount.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount > Number(account.bank || 0)) {
|
||||
showNotice("error", "Bank balance cannot cover that withdrawal.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestWithdraw !== "function") {
|
||||
showNotice("error", "Withdraw bridge is unavailable.");
|
||||
@ -120,30 +88,8 @@
|
||||
|
||||
function requestTransfer(targetUid, amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const session = getSession();
|
||||
const account = getAccount();
|
||||
const targetId = String(targetUid || "").trim();
|
||||
|
||||
if (!targetId) {
|
||||
showNotice("error", "Select a transfer recipient.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetId === String(session.uid || "")) {
|
||||
showNotice("error", "You cannot transfer funds to yourself.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
showNotice("error", "Enter a valid transfer amount.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount > Number(account.bank || 0)) {
|
||||
showNotice("error", "Bank balance cannot cover that transfer.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestTransfer !== "function") {
|
||||
showNotice("error", "Transfer bridge is unavailable.");
|
||||
@ -167,21 +113,6 @@
|
||||
|
||||
function requestDepositEarnings(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const account = getAccount();
|
||||
|
||||
if (amount <= 0) {
|
||||
showNotice("error", "No earnings are available to deposit.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (amount > Number(account.earnings || 0)) {
|
||||
showNotice(
|
||||
"error",
|
||||
"Pending earnings cannot cover that deposit request.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestDepositEarnings !== "function") {
|
||||
showNotice("error", "Earnings bridge is unavailable.");
|
||||
@ -199,6 +130,25 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestRepayCreditLine(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestRepayCreditLine !== "function") {
|
||||
showNotice("error", "Credit repayment bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("repaycreditline");
|
||||
const sent = bridge.requestRepayCreditLine({ amount });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Credit repayment bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function appendPinDigit(digit) {
|
||||
const nextDigit = String(digit || "").trim();
|
||||
if (!nextDigit) {
|
||||
@ -224,21 +174,21 @@
|
||||
|
||||
function submitPin() {
|
||||
const enteredPin = String(store.getEnteredPin() || "");
|
||||
const actualPin = String(getAccount().pin || "1234");
|
||||
|
||||
if (enteredPin.length !== 4) {
|
||||
showNotice("error", "Enter your four-digit access PIN.");
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestSubmitPin !== "function") {
|
||||
showNotice("error", "PIN bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enteredPin !== actualPin) {
|
||||
clearPin();
|
||||
showNotice("error", "Incorrect PIN.");
|
||||
store.startAction("pin");
|
||||
const sent = bridge.requestSubmitPin({ pin: enteredPin });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "PIN bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPin();
|
||||
store.setAtmView("menu");
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -299,7 +249,6 @@
|
||||
|
||||
if (success) {
|
||||
store.setCustomAmount("");
|
||||
store.setAtmView("menu");
|
||||
}
|
||||
|
||||
return success;
|
||||
@ -314,10 +263,6 @@
|
||||
? requestDeposit(amount)
|
||||
: requestWithdraw(amount);
|
||||
|
||||
if (success) {
|
||||
store.setAtmView("menu");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@ -333,6 +278,7 @@
|
||||
requestAtmAmount,
|
||||
requestDeposit,
|
||||
requestDepositEarnings,
|
||||
requestRepayCreditLine,
|
||||
requestTransfer,
|
||||
requestWithdraw,
|
||||
selectAtmView,
|
||||
|
||||
@ -25,25 +25,46 @@
|
||||
const mode = String(payload?.session?.mode || "bank")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const atmAuthorized = Boolean(payload?.session?.atmAuthorized);
|
||||
const currentMode = this.getMode();
|
||||
const currentAtmView = this.getAtmView();
|
||||
const currentPendingAction = this.getPendingAction();
|
||||
|
||||
this.setMode(mode === "atm" ? "atm" : "bank");
|
||||
this.setPendingAction("");
|
||||
this.setNotice({ text: "", type: "" });
|
||||
this.setEnteredPin("");
|
||||
this.setCustomAmount("");
|
||||
this.setAccountVersion(this.getAccountVersion() + 1);
|
||||
this.setSessionVersion(this.getSessionVersion() + 1);
|
||||
|
||||
if (mode === "atm") {
|
||||
this.setAtmView(currentMode === "atm" ? currentAtmView : "pin");
|
||||
if (!atmAuthorized) {
|
||||
this.setAtmView("pin");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
currentPendingAction === "deposit" ||
|
||||
currentPendingAction === "withdraw" ||
|
||||
currentAtmView === "pin" ||
|
||||
currentMode !== "atm"
|
||||
) {
|
||||
this.setAtmView("menu");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setAtmView(currentAtmView);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setAtmView("dashboard");
|
||||
}
|
||||
|
||||
syncAccountPatch() {
|
||||
this.setPendingAction("");
|
||||
this.setAccountVersion(this.getAccountVersion() + 1);
|
||||
}
|
||||
|
||||
resetAtm() {
|
||||
this.setEnteredPin("");
|
||||
this.setCustomAmount("");
|
||||
|
||||
1
arma/client/addons/cad/$PBOPREFIX$
Normal file
1
arma/client/addons/cad/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\cad
|
||||
11
arma/client/addons/cad/CfgEventHandlers.hpp
Normal file
11
arma/client/addons/cad/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,11 @@
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
||||
};
|
||||
};
|
||||
214
arma/client/addons/cad/MAP_README.md
Normal file
214
arma/client/addons/cad/MAP_README.md
Normal file
@ -0,0 +1,214 @@
|
||||
# Integrated Map Display System (A3API Pattern)
|
||||
|
||||
This system integrates the Arma 3 native map control (`RscMapControl`) within an HTML/CSS/JS UI using Arma's proper WebBrowser control (type 106) and A3API communication pattern.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
1. **IFrame Control (type 106)** - Loads HTML content using `ctrlWebBrowserAction`
|
||||
2. **Map Control (RscMapControl)** - Native Arma map positioned behind/within the UI
|
||||
3. **A3API Communication** - Bidirectional communication between JavaScript and SQF
|
||||
|
||||
### Communication Flow
|
||||
|
||||
**JavaScript → SQF:**
|
||||
```javascript
|
||||
// Send alert (no response expected)
|
||||
A3API.SendAlert(JSON.stringify({
|
||||
event: "map::zoomIn",
|
||||
data: null
|
||||
}));
|
||||
|
||||
// Send confirm (expects response via ExecJS)
|
||||
A3API.SendConfirm(JSON.stringify({
|
||||
event: "map::getPosition",
|
||||
data: null
|
||||
}));
|
||||
```
|
||||
|
||||
**SQF → JavaScript:**
|
||||
```sqf
|
||||
_control ctrlWebBrowserAction ["ExecJS", "updateMapState({center: [1000, 2000], scale: 0.5});"];
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
UI/map/
|
||||
├── _site/
|
||||
│ ├── index.html # HTML with A3API dynamic loading
|
||||
│ ├── script.js # JavaScript using A3API
|
||||
│ └── style.css # Styling
|
||||
└── MAP_README.md # This file
|
||||
|
||||
functions/map/
|
||||
├── fn_openMap.sqf # Opens the display
|
||||
├── fn_mapHandleUIEvents.sqf # Handles JS events
|
||||
├── fn_mapDisplay.sqf # Display initialization
|
||||
└── fn_mapDisplayUpdate.sqf # Update loop
|
||||
|
||||
UI/MapDisplay.h # Dialog definition
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Opening the Map
|
||||
|
||||
```sqf
|
||||
[] call FORGE_fnc_openMap;
|
||||
```
|
||||
|
||||
### From Init or Action
|
||||
|
||||
```sqf
|
||||
// Add player action
|
||||
player addAction ["Open Map", {[] call FORGE_fnc_openMap;}];
|
||||
|
||||
// In init.sqf
|
||||
[] call FORGE_fnc_openMap;
|
||||
```
|
||||
|
||||
## Key Differences from Standard HTML/CSS/JS
|
||||
|
||||
### 1. Dynamic Resource Loading
|
||||
|
||||
Instead of `<link>` and `<script>` tags, files are loaded using A3API:
|
||||
|
||||
```html
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile("UI\\map\\_site\\style.css"),
|
||||
A3API.RequestFile("UI\\map\\_site\\script.js")
|
||||
]).then(([css, js]) => {
|
||||
// Apply CSS
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Execute JavaScript
|
||||
const script = document.createElement('script');
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. Event Communication
|
||||
|
||||
Use **A3API.SendAlert()** for one-way messages:
|
||||
```javascript
|
||||
A3API.SendAlert(JSON.stringify({event: "map::action", data: value}));
|
||||
```
|
||||
|
||||
Use **A3API.SendConfirm()** for messages expecting a response:
|
||||
```javascript
|
||||
A3API.SendConfirm(JSON.stringify({event: "map::getdata", data: null}));
|
||||
```
|
||||
|
||||
### 3. Pointer Events
|
||||
|
||||
UI elements need `pointer-events: auto` while the body has `pointer-events: none`:
|
||||
|
||||
```css
|
||||
body {
|
||||
pointer-events: none; /* Allows clicks through to map */
|
||||
}
|
||||
|
||||
#topBar {
|
||||
pointer-events: auto; /* UI elements catch clicks */
|
||||
}
|
||||
```
|
||||
|
||||
## Dialog Definition Pattern
|
||||
|
||||
```cpp
|
||||
class RscMapDisplay {
|
||||
idd = 9000;
|
||||
onLoad = "['onLoad', _this] call FORGE_fnc_mapDisplay;";
|
||||
|
||||
class Controls {
|
||||
class Browser: RscText {
|
||||
type = 106; // IFrame control type
|
||||
idc = 9001;
|
||||
x = "safeZoneX";
|
||||
y = "safeZoneY";
|
||||
w = "safeZoneW";
|
||||
h = "safeZoneH";
|
||||
};
|
||||
|
||||
class MapControl: RscMapControl {
|
||||
idc = 9002;
|
||||
// Position to fit within HTML UI
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Event Handler Pattern
|
||||
|
||||
In `fn_openMap.sqf`:
|
||||
```sqf
|
||||
private _ctrl = _display displayCtrl 9001;
|
||||
|
||||
// Add JSDialog event handler
|
||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
[_control, _isConfirmDialog, _message] call FORGE_fnc_mapHandleUIEvents;
|
||||
}];
|
||||
|
||||
// Load HTML file
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", "UI\\map\\_site\\index.html"];
|
||||
```
|
||||
|
||||
In `fn_mapHandleUIEvents.sqf`:
|
||||
```sqf
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
private _eventData = fromJSON _message;
|
||||
private _event = _eventData get "event";
|
||||
private _data = _eventData get "data";
|
||||
|
||||
switch (_event) do {
|
||||
case "map::ready": {
|
||||
// Initialize
|
||||
};
|
||||
case "map::zoomIn": {
|
||||
// Handle zoom
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Benefits of This Pattern
|
||||
|
||||
1. **Proper Arma Integration** - Uses native WebBrowser control (type 106)
|
||||
2. **File System Compatibility** - A3API.RequestFile() works with Arma's file system
|
||||
3. **Reliable Communication** - JSDialog event handler is more stable than htmlLoad
|
||||
4. **Modular** - CSS and JS in separate files, dynamically loaded
|
||||
5. **Consistent** - Matches bank module pattern used in FORGE
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Files not loading:**
|
||||
- Check paths use double backslashes: `"UI\\map\\_site\\style.css"`
|
||||
- Verify files exist in the correct directory
|
||||
- Check .rpt log for file loading errors
|
||||
|
||||
**Events not firing:**
|
||||
- Verify JSDialog event handler is attached
|
||||
- Check JSON formatting in A3API calls
|
||||
- Look for JavaScript console errors (use OpenDevConsole)
|
||||
|
||||
**Map not showing:**
|
||||
- Verify MapControl idc matches (9002)
|
||||
- Check map control positioning in MapDisplay.h
|
||||
- Ensure map control is rendered after browser control
|
||||
|
||||
## Developer Tools
|
||||
|
||||
Enable dev console in `fn_openMap.sqf`:
|
||||
```sqf
|
||||
_ctrl ctrlWebBrowserAction ["OpenDevConsole"];
|
||||
```
|
||||
|
||||
This opens Chromium dev tools for debugging JavaScript, CSS, and network requests.
|
||||
5
arma/client/addons/cad/XEH_PREP.hpp
Normal file
5
arma/client/addons/cad/XEH_PREP.hpp
Normal file
@ -0,0 +1,5 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initRepository);
|
||||
PREP(initUIBridge);
|
||||
PREP(initUI);
|
||||
PREP(openUI);
|
||||
40
arma/client/addons/cad/XEH_postInitClient.sqf
Normal file
40
arma/client/addons/cad/XEH_postInitClient.sqf
Normal file
@ -0,0 +1,40 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(CADRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(CADUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(openCAD), {
|
||||
call FUNC(openUI);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseHydrateCad), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleHydrateResponse", [_payload]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCadAssignment), {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleAssignmentResponse", [_result]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCadGroupUpdate), {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleGroupUpdateResponse", [_result]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCadRequest), {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(CADUIBridge) call ["handleRequestResponse", [_result]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(invalidateCadState), {
|
||||
if (isNil QGVAR(CADRepository)) exitWith {};
|
||||
if !(GVAR(CADRepository) getOrDefault ["isOpen", false]) exitWith {};
|
||||
if (isNil QGVAR(CADUIBridge)) exitWith {};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestHydrate", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
5
arma/client/addons/cad/XEH_preInit.sqf
Normal file
5
arma/client/addons/cad/XEH_preInit.sqf
Normal file
@ -0,0 +1,5 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
1
arma/client/addons/cad/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/cad/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
21
arma/client/addons/cad/config.cpp
Normal file
21
arma/client/addons/cad/config.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"IDSolutions"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "ui\RscCommon.hpp"
|
||||
#include "ui\RscMapUI.hpp"
|
||||
229
arma/client/addons/cad/functions/fnc_handleUIEvents.sqf
Normal file
229
arma/client/addons/cad/functions/fnc_handleUIEvents.sqf
Normal file
@ -0,0 +1,229 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_handleUIEvents.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-28
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Handles CAD browser UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Control [CONTROL]
|
||||
* 1: Confirm dialog flag [BOOL]
|
||||
* 2: Browser message [STRING]
|
||||
*
|
||||
* Return Value:
|
||||
* UI event handled [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* [_control, false, _message] call forge_client_cad_fnc_handleUIEvents
|
||||
*/
|
||||
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
private _alert = fromJSON _message;
|
||||
private _event = _alert getOrDefault ["event", ""];
|
||||
private _data = _alert getOrDefault ["data", nil];
|
||||
|
||||
diag_log format ["[FORGE:Client:CAD] Handling UI event: %1", _event];
|
||||
|
||||
if (_isConfirmDialog) exitWith { true };
|
||||
|
||||
switch (_event) do {
|
||||
case "cad::topbar::ready": {
|
||||
GVAR(CADUIBridge) call ["handleTopBarReady", []];
|
||||
};
|
||||
case "cad::ready": {
|
||||
GVAR(CADUIBridge) call ["handleReady", [_control, _data]];
|
||||
};
|
||||
case "cad::dispatcher::ready": {
|
||||
GVAR(CADUIBridge) call ["handleDispatcherReady", []];
|
||||
};
|
||||
case "cad::mode::set": {
|
||||
private _mode = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_mode = _data getOrDefault ["mode", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["setMode", [_mode]];
|
||||
};
|
||||
case "cad::dispatchView::set": {
|
||||
private _dispatchView = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_dispatchView = _data getOrDefault ["dispatchView", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["setDispatchView", [_dispatchView]];
|
||||
};
|
||||
case "cad::refresh": {
|
||||
GVAR(CADUIBridge) call ["requestHydrate", []];
|
||||
};
|
||||
case "cad::tasks::assign": {
|
||||
private _taskID = "";
|
||||
private _groupID = "";
|
||||
private _note = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_taskID = _data getOrDefault ["taskID", ""];
|
||||
_groupID = _data getOrDefault ["groupID", ""];
|
||||
_note = _data getOrDefault ["note", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestAssignTask", [_taskID, _groupID, _note]];
|
||||
};
|
||||
case "cad::dispatchOrder::create": {
|
||||
private _assigneeGroupID = "";
|
||||
private _targetGroupID = "";
|
||||
private _note = "";
|
||||
private _priority = "priority";
|
||||
private _request = createHashMap;
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_assigneeGroupID = _data getOrDefault ["assigneeGroupID", ""];
|
||||
_targetGroupID = _data getOrDefault ["targetGroupID", ""];
|
||||
_note = _data getOrDefault ["note", ""];
|
||||
_priority = _data getOrDefault ["priority", "priority"];
|
||||
_request = _data getOrDefault ["request", createHashMap];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestCreateDispatchOrder", [_assigneeGroupID, _targetGroupID, _note, _priority, _request]];
|
||||
};
|
||||
case "cad::supportRequest::submit": {
|
||||
private _type = "";
|
||||
private _fields = createHashMap;
|
||||
private _priority = "priority";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_type = _data getOrDefault ["type", ""];
|
||||
_fields = _data getOrDefault ["fields", createHashMap];
|
||||
_priority = _data getOrDefault ["priority", "priority"];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestSubmitSupportRequest", [_type, _fields, _priority]];
|
||||
};
|
||||
case "cad::dispatchOrder::close": {
|
||||
private _taskID = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_taskID = _data getOrDefault ["taskID", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestCloseDispatchOrder", [_taskID]];
|
||||
};
|
||||
case "cad::supportRequest::close": {
|
||||
private _requestID = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_requestID = _data getOrDefault ["requestID", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestCloseSupportRequest", [_requestID]];
|
||||
};
|
||||
case "cad::tasks::acknowledge": {
|
||||
private _taskID = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_taskID = _data getOrDefault ["taskID", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestAcknowledgeTask", [_taskID]];
|
||||
};
|
||||
case "cad::tasks::decline": {
|
||||
private _taskID = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_taskID = _data getOrDefault ["taskID", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestDeclineTask", [_taskID]];
|
||||
};
|
||||
case "cad::groups::status": {
|
||||
private _groupID = "";
|
||||
private _status = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_groupID = _data getOrDefault ["groupID", ""];
|
||||
_status = _data getOrDefault ["status", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestGroupStatus", [_groupID, _status]];
|
||||
};
|
||||
case "cad::groups::role": {
|
||||
private _groupID = "";
|
||||
private _role = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_groupID = _data getOrDefault ["groupID", ""];
|
||||
_role = _data getOrDefault ["role", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestGroupRole", [_groupID, _role]];
|
||||
};
|
||||
case "cad::groups::profile": {
|
||||
private _groupID = "";
|
||||
private _status = "";
|
||||
private _role = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_groupID = _data getOrDefault ["groupID", ""];
|
||||
_status = _data getOrDefault ["status", ""];
|
||||
_role = _data getOrDefault ["role", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["requestGroupProfile", [_groupID, _status, _role]];
|
||||
};
|
||||
case "cad::groups::focus": {
|
||||
private _groupID = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_groupID = _data getOrDefault ["groupID", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["focusGroup", [_groupID]];
|
||||
};
|
||||
case "cad::tasks::focus": {
|
||||
private _taskID = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_taskID = _data getOrDefault ["taskID", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["focusTask", [_taskID]];
|
||||
};
|
||||
case "cad::requests::focus": {
|
||||
private _requestID = "";
|
||||
if (_data isEqualType createHashMap) then {
|
||||
_requestID = _data getOrDefault ["requestID", ""];
|
||||
};
|
||||
|
||||
GVAR(CADUIBridge) call ["focusRequest", [_requestID]];
|
||||
};
|
||||
case "map::zoomIn": {
|
||||
private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull];
|
||||
if (isNull _mapCtrl) exitWith {};
|
||||
|
||||
private _currentZoom = ctrlMapScale _mapCtrl;
|
||||
private _newZoom = (_currentZoom * 0.5) max 0.001;
|
||||
private _center = _mapCtrl ctrlMapScreenToWorld [0.5, 0.5];
|
||||
_mapCtrl ctrlMapAnimAdd [0.3, _newZoom, _center];
|
||||
ctrlMapAnimCommit _mapCtrl;
|
||||
};
|
||||
case "map::zoomOut": {
|
||||
private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull];
|
||||
if (isNull _mapCtrl) exitWith {};
|
||||
|
||||
private _currentZoom = ctrlMapScale _mapCtrl;
|
||||
private _newZoom = (_currentZoom * 2) min 1;
|
||||
private _center = _mapCtrl ctrlMapScreenToWorld [0.5, 0.5];
|
||||
_mapCtrl ctrlMapAnimAdd [0.3, _newZoom, _center];
|
||||
ctrlMapAnimCommit _mapCtrl;
|
||||
};
|
||||
case "map::search": {
|
||||
private _query = str _data;
|
||||
private _bottomBar = uiNamespace getVariable [QGVAR(BottomBarCtrl), controlNull];
|
||||
if (isNull _bottomBar) exitWith {};
|
||||
|
||||
_bottomBar ctrlWebBrowserAction ["ExecJS", format ["updateStatus('Search not yet implemented: %1');", _query]];
|
||||
};
|
||||
case "map::close": {
|
||||
if !(isNil QGVAR(CADUIBridge)) then {
|
||||
GVAR(CADUIBridge) call ["handleClose", []];
|
||||
};
|
||||
closeDialog 1;
|
||||
};
|
||||
default {
|
||||
diag_log format ["[FORGE:Client:CAD] WARNING: Unhandled UI event: %1", _event];
|
||||
};
|
||||
};
|
||||
|
||||
true
|
||||
105
arma/client/addons/cad/functions/fnc_initRepository.sqf
Normal file
105
arma/client/addons/cad/functions/fnc_initRepository.sqf
Normal file
@ -0,0 +1,105 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-28
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD repository for lightweight client lifecycle state.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* CAD repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_cad_fnc_initRepository
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(CADRepository) = createHashMapObject [[
|
||||
["#type", "CADRepository"],
|
||||
["#create", compileFinal {
|
||||
_self set ["isLoaded", true];
|
||||
_self set ["isOpen", false];
|
||||
_self set ["groups", []];
|
||||
_self set ["contracts", []];
|
||||
_self set ["requests", []];
|
||||
_self set ["assignments", []];
|
||||
_self set ["activity", []];
|
||||
_self set ["session", createHashMap];
|
||||
_self set ["mode", "operations"];
|
||||
_self set ["dispatchView", "board"];
|
||||
}],
|
||||
["getHydratePayload", compileFinal {
|
||||
createHashMapFromArray [
|
||||
["groups", +(_self getOrDefault ["groups", []])],
|
||||
["contracts", +(_self getOrDefault ["contracts", []])],
|
||||
["requests", +(_self getOrDefault ["requests", []])],
|
||||
["assignments", +(_self getOrDefault ["assignments", []])],
|
||||
["activity", +(_self getOrDefault ["activity", []])],
|
||||
["session", +(_self getOrDefault ["session", createHashMap])],
|
||||
["mode", _self getOrDefault ["mode", "operations"]],
|
||||
["dispatchView", _self getOrDefault ["dispatchView", "board"]]
|
||||
]
|
||||
}],
|
||||
["getCurrentGroup", compileFinal {
|
||||
private _session = _self getOrDefault ["session", createHashMap];
|
||||
private _groupID = _session getOrDefault ["groupId", ""];
|
||||
if (_groupID isEqualTo "") exitWith { createHashMap };
|
||||
|
||||
private _groups = _self getOrDefault ["groups", []];
|
||||
private _group = _groups findIf { (_x getOrDefault ["groupId", ""]) isEqualTo _groupID };
|
||||
if (_group < 0) exitWith { createHashMap };
|
||||
|
||||
+(_groups # _group)
|
||||
}],
|
||||
["pushHydratePayload", compileFinal {
|
||||
params [["_bridge", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_bridge isEqualTo createHashMap) exitWith { false };
|
||||
|
||||
_bridge call ["sendEvent", ["cad::hydrate", _self call ["getHydratePayload", []]]]
|
||||
}],
|
||||
["setHydratePayload", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
_self set ["groups", +(_payload getOrDefault ["groups", []])];
|
||||
_self set ["contracts", +(_payload getOrDefault ["contracts", []])];
|
||||
_self set ["requests", +(_payload getOrDefault ["requests", []])];
|
||||
_self set ["assignments", +(_payload getOrDefault ["assignments", []])];
|
||||
_self set ["activity", +(_payload getOrDefault ["activity", []])];
|
||||
_self set ["session", +(_payload getOrDefault ["session", createHashMap])];
|
||||
true
|
||||
}],
|
||||
["setMode", compileFinal {
|
||||
params [["_mode", "operations", [""]]];
|
||||
|
||||
if !(_mode in ["operations", "dispatch"]) then {
|
||||
_mode = "operations";
|
||||
};
|
||||
|
||||
_self set ["mode", _mode];
|
||||
_mode
|
||||
}],
|
||||
["setDispatchView", compileFinal {
|
||||
params [["_dispatchView", "board", [""]]];
|
||||
|
||||
if !(_dispatchView in ["board", "map"]) then {
|
||||
_dispatchView = "board";
|
||||
};
|
||||
|
||||
_self set ["dispatchView", _dispatchView];
|
||||
_dispatchView
|
||||
}],
|
||||
["setOpen", compileFinal {
|
||||
params [["_isOpen", false, [false]]];
|
||||
_self set ["isOpen", _isOpen];
|
||||
true
|
||||
}]
|
||||
]];
|
||||
|
||||
GVAR(CADRepository)
|
||||
51
arma/client/addons/cad/functions/fnc_initUI.sqf
Normal file
51
arma/client/addons/cad/functions/fnc_initUI.sqf
Normal file
@ -0,0 +1,51 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initUI.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-28
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD map dialog controls and local map event handling.
|
||||
*
|
||||
* Arguments:
|
||||
* 0: Display [DISPLAY]
|
||||
*
|
||||
* Return Value:
|
||||
* UI initialized [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* [_display] call forge_client_cad_fnc_initUI
|
||||
*/
|
||||
|
||||
params [["_display", displayNull, [displayNull]]];
|
||||
|
||||
if (isNull _display) exitWith { false };
|
||||
|
||||
private _mapCtrl = _display displayCtrl 1001;
|
||||
private _topBarCtrl = _display displayCtrl 1002;
|
||||
private _bottomBarCtrl = _display displayCtrl 1003;
|
||||
private _sidePanelCtrl = _display displayCtrl 1005;
|
||||
private _dispatcherCtrl = _display displayCtrl 1006;
|
||||
|
||||
uiNamespace setVariable [QGVAR(Display), _display];
|
||||
uiNamespace setVariable [QGVAR(MapCtrl), _mapCtrl];
|
||||
uiNamespace setVariable [QGVAR(TopBarCtrl), _topBarCtrl];
|
||||
uiNamespace setVariable [QGVAR(BottomBarCtrl), _bottomBarCtrl];
|
||||
uiNamespace setVariable [QGVAR(SidePanelCtrl), _sidePanelCtrl];
|
||||
uiNamespace setVariable [QGVAR(DispatcherCtrl), _dispatcherCtrl];
|
||||
|
||||
_dispatcherCtrl ctrlShow false;
|
||||
|
||||
private _center = if (isNull player) then {
|
||||
[worldSize / 2, worldSize / 2, 0]
|
||||
} else {
|
||||
getPosATL player
|
||||
};
|
||||
|
||||
_mapCtrl ctrlMapAnimAdd [0, 0.2, _center];
|
||||
ctrlMapAnimCommit _mapCtrl;
|
||||
|
||||
diag_log "[FORGE:Client:CAD] CAD UI initialized.";
|
||||
true
|
||||
450
arma/client/addons/cad/functions/fnc_initUIBridge.sqf
Normal file
450
arma/client/addons/cad/functions/fnc_initUIBridge.sqf
Normal file
@ -0,0 +1,450 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-28
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the CAD UI bridge for sidepanel browser state and CAD event routing.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* CAD UI bridge object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_cad_fnc_initUIBridge
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
private _webUIDeclarations = call EFUNC(common,initWebUIBridge);
|
||||
private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
|
||||
|
||||
GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#base", _webUIBridgeDeclaration],
|
||||
["#type", "CADUIBridgeBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["dispatcherReady", false];
|
||||
_self set ["topBarReady", false];
|
||||
}],
|
||||
["getActiveBrowserControl", compileFinal {
|
||||
private _display = uiNamespace getVariable [QGVAR(Display), displayNull];
|
||||
if (isNull _display) exitWith {
|
||||
_self call ["setActiveBrowserControl", [controlNull]];
|
||||
controlNull
|
||||
};
|
||||
|
||||
private _control = _display displayCtrl 1005;
|
||||
_self call ["setActiveBrowserControl", [_control]];
|
||||
_control
|
||||
}],
|
||||
["getTopBarControl", compileFinal {
|
||||
private _display = uiNamespace getVariable [QGVAR(Display), displayNull];
|
||||
if (isNull _display) exitWith { controlNull };
|
||||
|
||||
_display displayCtrl 1002
|
||||
}],
|
||||
["getBottomBarControl", compileFinal {
|
||||
private _display = uiNamespace getVariable [QGVAR(Display), displayNull];
|
||||
if (isNull _display) exitWith { controlNull };
|
||||
|
||||
_display displayCtrl 1003
|
||||
}],
|
||||
["getMapControl", compileFinal {
|
||||
private _display = uiNamespace getVariable [QGVAR(Display), displayNull];
|
||||
if (isNull _display) exitWith { controlNull };
|
||||
|
||||
_display displayCtrl 1001
|
||||
}],
|
||||
["getDispatcherControl", compileFinal {
|
||||
private _display = uiNamespace getVariable [QGVAR(Display), displayNull];
|
||||
if (isNull _display) exitWith { controlNull };
|
||||
|
||||
_display displayCtrl 1006
|
||||
}],
|
||||
["hasOpenScreen", compileFinal {
|
||||
private _screen = _self call ["getScreen", []];
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
!(isNull _control) && { _screen call ["isReady", []] }
|
||||
}],
|
||||
["isDispatcher", compileFinal {
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
private _session = GVAR(CADRepository) getOrDefault ["session", createHashMap];
|
||||
_session getOrDefault ["isDispatcher", false]
|
||||
}],
|
||||
["applyLayout", compileFinal {
|
||||
private _mode = if (isNil QGVAR(CADRepository)) then {
|
||||
"operations"
|
||||
} else {
|
||||
GVAR(CADRepository) getOrDefault ["mode", "operations"]
|
||||
};
|
||||
private _dispatchView = if (isNil QGVAR(CADRepository)) then {
|
||||
"board"
|
||||
} else {
|
||||
GVAR(CADRepository) getOrDefault ["dispatchView", "board"]
|
||||
};
|
||||
|
||||
private _mapCtrl = _self call ["getMapControl", []];
|
||||
private _bottomBarCtrl = _self call ["getBottomBarControl", []];
|
||||
private _sidePanelCtrl = _self call ["getActiveBrowserControl", []];
|
||||
private _dispatcherCtrl = _self call ["getDispatcherControl", []];
|
||||
|
||||
private _showMapLayout = (_mode isEqualTo "operations") || { _mode isEqualTo "dispatch" && { _dispatchView isEqualTo "map" } };
|
||||
|
||||
if !(isNull _mapCtrl) then { _mapCtrl ctrlShow _showMapLayout; };
|
||||
if !(isNull _bottomBarCtrl) then { _bottomBarCtrl ctrlShow true; };
|
||||
if !(isNull _sidePanelCtrl) then { _sidePanelCtrl ctrlShow _showMapLayout; };
|
||||
if !(isNull _dispatcherCtrl) then { _dispatcherCtrl ctrlShow (_mode isEqualTo "dispatch" && { _dispatchView isEqualTo "board" }); };
|
||||
|
||||
_self call ["refreshHydrate", []];
|
||||
_self call ["refreshTopBarState", []];
|
||||
_self call ["refreshDispatcher", []];
|
||||
true
|
||||
}],
|
||||
["setMode", compileFinal {
|
||||
params [["_mode", "operations", [""]]];
|
||||
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
private _targetMode = _mode;
|
||||
if !(_targetMode in ["operations", "dispatch"]) then {
|
||||
_targetMode = "operations";
|
||||
};
|
||||
|
||||
if (_targetMode isEqualTo "dispatch" && !(_self call ["isDispatcher", []])) then {
|
||||
_targetMode = "operations";
|
||||
};
|
||||
|
||||
GVAR(CADRepository) call ["setMode", [_targetMode]];
|
||||
if (_targetMode isEqualTo "dispatch") then {
|
||||
GVAR(CADRepository) call ["setDispatchView", ["board"]];
|
||||
};
|
||||
_self call ["applyLayout", []]
|
||||
}],
|
||||
["setDispatchView", compileFinal {
|
||||
params [["_dispatchView", "board", [""]]];
|
||||
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
if ((GVAR(CADRepository) getOrDefault ["mode", "operations"]) isNotEqualTo "dispatch") exitWith { false };
|
||||
if !(_self call ["isDispatcher", []]) exitWith { false };
|
||||
|
||||
GVAR(CADRepository) call ["setDispatchView", [_dispatchView]];
|
||||
_self call ["applyLayout", []]
|
||||
}],
|
||||
["refreshTopBarState", compileFinal {
|
||||
if !(_self getOrDefault ["topBarReady", false]) exitWith { false };
|
||||
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
private _topBarCtrl = _self call ["getTopBarControl", []];
|
||||
if (isNull _topBarCtrl) exitWith { false };
|
||||
|
||||
private _session = +(GVAR(CADRepository) getOrDefault ["session", createHashMap]);
|
||||
private _currentGroup = GVAR(CADRepository) call ["getCurrentGroup", []];
|
||||
private _payload = createHashMapFromArray [
|
||||
["mode", GVAR(CADRepository) getOrDefault ["mode", "operations"]],
|
||||
["dispatchView", GVAR(CADRepository) getOrDefault ["dispatchView", "board"]],
|
||||
["session", _session],
|
||||
["currentGroup", _currentGroup]
|
||||
];
|
||||
|
||||
_topBarCtrl ctrlWebBrowserAction ["ExecJS", format [
|
||||
"window.cadTopbar && window.cadTopbar.receiveState(%1);",
|
||||
toJSON _payload
|
||||
]];
|
||||
true
|
||||
}],
|
||||
["refreshDispatcher", compileFinal {
|
||||
if !(_self getOrDefault ["dispatcherReady", false]) exitWith { false };
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
private _dispatcherCtrl = _self call ["getDispatcherControl", []];
|
||||
if (isNull _dispatcherCtrl) exitWith { false };
|
||||
|
||||
private _payload = GVAR(CADRepository) call ["getHydratePayload", []];
|
||||
_dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [
|
||||
"window.cadDispatcher && window.cadDispatcher.receiveHydrate(%1);",
|
||||
toJSON _payload
|
||||
]];
|
||||
true
|
||||
}],
|
||||
["handleReady", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _screen = _self call ["getScreen", []];
|
||||
_screen call ["setControl", [_control]];
|
||||
_screen call ["markReady", [true]];
|
||||
_self call ["flushPendingEvents", []];
|
||||
|
||||
_self call ["requestHydrate", []];
|
||||
_self call ["refreshHydrate", []];
|
||||
_self call ["refreshTopBarState", []];
|
||||
true
|
||||
}],
|
||||
["handleClose", compileFinal {
|
||||
_self set ["dispatcherReady", false];
|
||||
_self set ["topBarReady", false];
|
||||
|
||||
private _screen = _self call ["getScreen", []];
|
||||
_screen call ["dispose", []];
|
||||
true
|
||||
}],
|
||||
["handleTopBarReady", compileFinal {
|
||||
_self set ["topBarReady", true];
|
||||
_self call ["refreshTopBarState", []]
|
||||
}],
|
||||
["handleDispatcherReady", compileFinal {
|
||||
_self set ["dispatcherReady", true];
|
||||
_self call ["refreshDispatcher", []]
|
||||
}],
|
||||
["requestHydrate", compileFinal {
|
||||
[SRPC(cad,requestHydrateCad), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestAssignTask", compileFinal {
|
||||
params [["_taskID", "", [""]], ["_groupID", "", [""]], ["_note", "", [""]]];
|
||||
|
||||
if (_taskID isEqualTo "" || { _groupID isEqualTo "" }) exitWith { false };
|
||||
|
||||
[SRPC(cad,requestAssignCadTask), [getPlayerUID player, _taskID, _groupID, _note]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestCreateDispatchOrder", compileFinal {
|
||||
params [
|
||||
["_assigneeGroupID", "", [""]],
|
||||
["_targetGroupID", "", [""]],
|
||||
["_note", "", [""]],
|
||||
["_priority", "priority", [""]],
|
||||
["_request", createHashMap, [createHashMap]]
|
||||
];
|
||||
|
||||
if (_assigneeGroupID isEqualTo "" || { _targetGroupID isEqualTo "" }) exitWith { false };
|
||||
|
||||
[SRPC(cad,requestCreateCadDispatchOrder), [getPlayerUID player, _assigneeGroupID, _targetGroupID, _note, _priority, _request]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestSubmitSupportRequest", compileFinal {
|
||||
params [
|
||||
["_type", "", [""]],
|
||||
["_fields", createHashMap, [createHashMap]],
|
||||
["_priority", "priority", [""]]
|
||||
];
|
||||
|
||||
if (_type isEqualTo "") exitWith { false };
|
||||
|
||||
[SRPC(cad,requestSubmitCadSupportRequest), [getPlayerUID player, _type, _fields, _priority]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestCloseDispatchOrder", compileFinal {
|
||||
params [["_taskID", "", [""]]];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
[SRPC(cad,requestCloseCadDispatchOrder), [getPlayerUID player, _taskID]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestCloseSupportRequest", compileFinal {
|
||||
params [["_requestID", "", [""]]];
|
||||
|
||||
if (_requestID isEqualTo "") exitWith { false };
|
||||
|
||||
[SRPC(cad,requestCloseCadSupportRequest), [getPlayerUID player, _requestID]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestAcknowledgeTask", compileFinal {
|
||||
params [["_taskID", "", [""]]];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
[SRPC(cad,requestAcknowledgeCadTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestDeclineTask", compileFinal {
|
||||
params [["_taskID", "", [""]]];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
|
||||
[SRPC(cad,requestDeclineCadTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestGroupStatus", compileFinal {
|
||||
params [["_groupID", "", [""]], ["_status", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "" || { _status isEqualTo "" }) exitWith { false };
|
||||
|
||||
[SRPC(cad,requestUpdateCadGroupStatus), [getPlayerUID player, _groupID, _status]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestGroupRole", compileFinal {
|
||||
params [["_groupID", "", [""]], ["_role", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "" || { _role isEqualTo "" }) exitWith { false };
|
||||
|
||||
[SRPC(cad,requestUpdateCadGroupRole), [getPlayerUID player, _groupID, _role]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["requestGroupProfile", compileFinal {
|
||||
params [["_groupID", "", [""]], ["_status", "", [""]], ["_role", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "") exitWith { false };
|
||||
if (_status isEqualTo "" && { _role isEqualTo "" }) exitWith { false };
|
||||
|
||||
[SRPC(cad,requestUpdateCadGroupProfile), [getPlayerUID player, _groupID, _status, _role]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["focusGroup", compileFinal {
|
||||
params [["_groupID", "", [""]]];
|
||||
|
||||
if (_groupID isEqualTo "") exitWith { false };
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
private _groups = GVAR(CADRepository) getOrDefault ["groups", []];
|
||||
private _groupIndex = _groups findIf { (_x getOrDefault ["groupId", ""]) isEqualTo _groupID };
|
||||
if (_groupIndex < 0) exitWith { false };
|
||||
|
||||
private _group = _groups # _groupIndex;
|
||||
private _position = _group getOrDefault ["position", []];
|
||||
if !(_position isEqualType []) exitWith { false };
|
||||
if ((count _position) < 2) exitWith { false };
|
||||
|
||||
private _mapCtrl = _self call ["getMapControl", []];
|
||||
if (isNull _mapCtrl) exitWith { false };
|
||||
|
||||
private _targetPosition = [_position # 0, _position # 1, 0];
|
||||
_mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition];
|
||||
ctrlMapAnimCommit _mapCtrl;
|
||||
true
|
||||
}],
|
||||
["focusTask", compileFinal {
|
||||
params [["_taskID", "", [""]]];
|
||||
|
||||
if (_taskID isEqualTo "") exitWith { false };
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
private _contracts = GVAR(CADRepository) getOrDefault ["contracts", []];
|
||||
private _taskIndex = _contracts findIf {
|
||||
private _entryTaskID = _x getOrDefault ["taskId", _x getOrDefault ["taskID", ""]];
|
||||
_entryTaskID isEqualTo _taskID
|
||||
};
|
||||
if (_taskIndex < 0) exitWith { false };
|
||||
|
||||
private _task = _contracts # _taskIndex;
|
||||
private _position = _task getOrDefault ["position", []];
|
||||
if !(_position isEqualType []) exitWith { false };
|
||||
if ((count _position) < 2) exitWith { false };
|
||||
|
||||
private _mapCtrl = _self call ["getMapControl", []];
|
||||
if (isNull _mapCtrl) exitWith { false };
|
||||
|
||||
private _targetPosition = [_position # 0, _position # 1, 0];
|
||||
_mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition];
|
||||
ctrlMapAnimCommit _mapCtrl;
|
||||
true
|
||||
}],
|
||||
["focusRequest", compileFinal {
|
||||
params [["_requestID", "", [""]]];
|
||||
|
||||
if (_requestID isEqualTo "") exitWith { false };
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
private _requests = GVAR(CADRepository) getOrDefault ["requests", []];
|
||||
private _requestIndex = _requests findIf { (_x getOrDefault ["requestId", ""]) isEqualTo _requestID };
|
||||
if (_requestIndex < 0) exitWith { false };
|
||||
|
||||
private _request = _requests # _requestIndex;
|
||||
private _position = _request getOrDefault ["position", []];
|
||||
if !(_position isEqualType []) exitWith { false };
|
||||
if ((count _position) < 2) exitWith { false };
|
||||
|
||||
private _mapCtrl = _self call ["getMapControl", []];
|
||||
if (isNull _mapCtrl) exitWith { false };
|
||||
|
||||
private _targetPosition = [_position # 0, _position # 1, 0];
|
||||
_mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition];
|
||||
ctrlMapAnimCommit _mapCtrl;
|
||||
true
|
||||
}],
|
||||
["refreshHydrate", compileFinal {
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
GVAR(CADRepository) call ["pushHydratePayload", [_self]]
|
||||
}],
|
||||
["handleHydrateResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
if (isNil QGVAR(CADRepository)) exitWith { false };
|
||||
|
||||
GVAR(CADRepository) call ["setHydratePayload", [_payload]];
|
||||
if !(_self call ["isDispatcher", []]) then {
|
||||
GVAR(CADRepository) call ["setMode", ["operations"]];
|
||||
};
|
||||
|
||||
_self call ["refreshHydrate", []];
|
||||
_self call ["refreshTopBarState", []];
|
||||
_self call ["refreshDispatcher", []];
|
||||
_self call ["applyLayout", []]
|
||||
}],
|
||||
["handleAssignmentResponse", compileFinal {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_self getOrDefault ["dispatcherReady", false]) then {
|
||||
private _dispatcherCtrl = _self call ["getDispatcherControl", []];
|
||||
if !(isNull _dispatcherCtrl) then {
|
||||
_dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [
|
||||
"window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);",
|
||||
str (_result getOrDefault ["message", "Task request processed."]),
|
||||
str ([ "error", "success" ] select (_result getOrDefault ["success", false]))
|
||||
]];
|
||||
};
|
||||
};
|
||||
|
||||
_self call ["sendEvent", ["cad::assignment::response", createHashMapFromArray [
|
||||
["message", _result getOrDefault ["message", "Task request processed."]],
|
||||
["success", _result getOrDefault ["success", false]]
|
||||
]]]
|
||||
}],
|
||||
["handleGroupUpdateResponse", compileFinal {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_self getOrDefault ["dispatcherReady", false]) then {
|
||||
private _dispatcherCtrl = _self call ["getDispatcherControl", []];
|
||||
if !(isNull _dispatcherCtrl) then {
|
||||
_dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [
|
||||
"window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);",
|
||||
str (_result getOrDefault ["message", "Group update processed."]),
|
||||
str ([ "error", "success" ] select (_result getOrDefault ["success", false]))
|
||||
]];
|
||||
};
|
||||
};
|
||||
|
||||
_self call ["sendEvent", ["cad::group::response", createHashMapFromArray [
|
||||
["message", _result getOrDefault ["message", "Group update processed."]],
|
||||
["success", _result getOrDefault ["success", false]]
|
||||
]]]
|
||||
}],
|
||||
["handleRequestResponse", compileFinal {
|
||||
params [["_result", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_self getOrDefault ["dispatcherReady", false]) then {
|
||||
private _dispatcherCtrl = _self call ["getDispatcherControl", []];
|
||||
if !(isNull _dispatcherCtrl) then {
|
||||
_dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [
|
||||
"window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);",
|
||||
str (_result getOrDefault ["message", "Request processed."]),
|
||||
str (["error", "success"] select (_result getOrDefault ["success", false]))
|
||||
]];
|
||||
};
|
||||
};
|
||||
|
||||
_self call ["sendEvent", ["cad::request::response", createHashMapFromArray [
|
||||
["message", _result getOrDefault ["message", "Request processed."]],
|
||||
["success", _result getOrDefault ["success", false]]
|
||||
]]]
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(CADUIBridge) = createHashMapObject [GVAR(CADUIBridgeBaseClass)];
|
||||
GVAR(CADUIBridge)
|
||||
49
arma/client/addons/cad/functions/fnc_openUI.sqf
Normal file
49
arma/client/addons/cad/functions/fnc_openUI.sqf
Normal file
@ -0,0 +1,49 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_openUI.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-28
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Opens the CAD map interface.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* UI opened [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_cad_fnc_openUI
|
||||
*/
|
||||
|
||||
private _display = createDialog ["RscMapUI", true];
|
||||
if (isNull _display) exitWith {
|
||||
diag_log "[FORGE:Client:CAD] ERROR: Failed to create CAD dialog.";
|
||||
false
|
||||
};
|
||||
|
||||
private _topBarCtrl = _display displayCtrl 1002;
|
||||
private _bottomBarCtrl = _display displayCtrl 1003;
|
||||
private _sidePanelCtrl = _display displayCtrl 1005;
|
||||
private _dispatcherCtrl = _display displayCtrl 1006;
|
||||
|
||||
{
|
||||
_x ctrlAddEventHandler ["JSDialog", {
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
||||
}];
|
||||
} forEach [_topBarCtrl, _bottomBarCtrl, _sidePanelCtrl, _dispatcherCtrl];
|
||||
|
||||
_topBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\topbar.html)];
|
||||
_bottomBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\bottombar.html)];
|
||||
_sidePanelCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\sidepanel.html)];
|
||||
_dispatcherCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\dispatcher.html)];
|
||||
|
||||
if !(isNil QGVAR(CADRepository)) then {
|
||||
GVAR(CADRepository) call ["setOpen", [true]];
|
||||
};
|
||||
|
||||
true
|
||||
9
arma/client/addons/cad/script_component.hpp
Normal file
9
arma/client/addons/cad/script_component.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#define COMPONENT cad
|
||||
#define COMPONENT_BEAUTIFIED CAD
|
||||
#include "\forge\forge_client\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#include "\forge\forge_client\addons\main\script_macros.hpp"
|
||||
6
arma/client/addons/cad/ui/RscCommon.hpp
Normal file
6
arma/client/addons/cad/ui/RscCommon.hpp
Normal file
@ -0,0 +1,6 @@
|
||||
// Control types
|
||||
#define CT_STATIC 0
|
||||
#define CT_MAP 100
|
||||
|
||||
class RscText;
|
||||
class RscMapControl;
|
||||
109
arma/client/addons/cad/ui/RscMapUI.hpp
Normal file
109
arma/client/addons/cad/ui/RscMapUI.hpp
Normal file
@ -0,0 +1,109 @@
|
||||
class RscMapUI {
|
||||
idd = 1004;
|
||||
movingEnable = 0;
|
||||
enableSimulation = 1;
|
||||
fadein = 0;
|
||||
fadeout = 0;
|
||||
duration = 1e+011;
|
||||
onLoad = "uiNamespace setVariable ['forge_client_cad_Display', _this select 0]; [_this select 0] call forge_client_cad_fnc_initUI;";
|
||||
onUnLoad = "uiNamespace setVariable ['forge_client_cad_Display', nil]; uiNamespace setVariable ['forge_client_cad_MapCtrl', nil]; uiNamespace setVariable ['forge_client_cad_TopBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_BottomBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_SidePanelCtrl', nil]; uiNamespace setVariable ['forge_client_cad_DispatcherCtrl', nil]; if !(isNil 'forge_client_cad_CADRepository') then { forge_client_cad_CADRepository set ['isOpen', false]; };";
|
||||
|
||||
class controlsBackground {
|
||||
class SurfaceBackground: RscText {
|
||||
idc = -1;
|
||||
x = "safeZoneX + (safeZoneW * 0.1)";
|
||||
y = "safeZoneY + (safeZoneH * 0.1)";
|
||||
w = "safeZoneW * 0.8";
|
||||
h = "safeZoneH * 0.8";
|
||||
colorBackground[] = {0.04, 0.06, 0.09, 0.96};
|
||||
};
|
||||
|
||||
class MapControl: RscMapControl {
|
||||
idc = 1001;
|
||||
x = "safeZoneX + (safeZoneW * 0.1)"; // 10% margin (80% width centered)
|
||||
y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; // 10% margin + 56px visible top bar
|
||||
w = "safeZoneW * 0.8"; // 80% width
|
||||
h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; // 80% height minus visible top and bottom bars
|
||||
|
||||
// Map specific settings
|
||||
maxSatelliteAlpha = 0.85;
|
||||
alphaFadeStartScale = 0.35;
|
||||
alphaFadeEndScale = 0.4;
|
||||
colorBackground[] = {0.969, 0.957, 0.949, 1};
|
||||
colorSea[] = {0.467, 0.631, 0.851, 0.5};
|
||||
colorForest[] = {0.624, 0.78, 0.388, 0.5};
|
||||
colorRocks[] = {0, 0, 0, 0};
|
||||
colorCountlines[] = {0.572, 0.354, 0.318, 0.25};
|
||||
colorMainCountlines[] = {0.572, 0.354, 0.318, 0.5};
|
||||
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.286, 0.177, 0.094, 0.5};
|
||||
colorTracks[] = {0.84, 0.76, 0.65, 0.15};
|
||||
colorRoads[] = {0.7, 0.7, 0.7, 1};
|
||||
colorMainRoads[] = {0.9, 0.5, 0.3, 1};
|
||||
colorTracksFill[] = {0.84, 0.76, 0.65, 1};
|
||||
colorRoadsFill[] = {1, 1, 1, 1};
|
||||
colorMainRoadsFill[] = {1, 0.6, 0.4, 1};
|
||||
colorGrid[] = {0.1, 0.1, 0.1, 0.6};
|
||||
colorGridMap[] = {0.1, 0.1, 0.1, 0.6};
|
||||
colorText[] = {1, 1, 1, 1};
|
||||
font = "PuristaMedium";
|
||||
sizeEx = 0.04;
|
||||
showCountourInterval = 0;
|
||||
scaleMin = 0.001;
|
||||
scaleMax = 1;
|
||||
scaleDefault = 0.16;
|
||||
};
|
||||
};
|
||||
|
||||
class controls {
|
||||
// Top bar browser
|
||||
class TopBarBrowser: RscText {
|
||||
type = 106;
|
||||
idc = 1002;
|
||||
x = "safeZoneX + (safeZoneW * 0.1)";
|
||||
y = "safeZoneY + (safeZoneH * 0.1)";
|
||||
w = "safeZoneW * 0.8";
|
||||
h = "0.24076"; // 130px, allows dropdowns to open over the map
|
||||
colorBackground[] = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Bottom bar browser
|
||||
class BottomBarBrowser: RscText {
|
||||
type = 106;
|
||||
idc = 1003;
|
||||
x = "safeZoneX + (safeZoneW * 0.1)";
|
||||
y = "safeZoneY + (safeZoneH * 0.9) - 0.0556";
|
||||
w = "safeZoneW * 0.8";
|
||||
h = "0.0556"; // 30px
|
||||
colorBackground[] = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
// Side panel browser (overlays from right side of 80% box)
|
||||
class SidePanelBrowser: RscText {
|
||||
type = 106;
|
||||
idc = 1005;
|
||||
x = "safeZoneX + (safeZoneW * 0.1) + (safeZoneW * 0.8) - 0.5550"; // Right edge of 80% box minus panel width
|
||||
y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; // Below visible top bar
|
||||
w = "0.5550"; // Wider panel for four-tab operations layout
|
||||
h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; // Full height minus visible bars
|
||||
colorBackground[] = {0, 0, 0, 0};
|
||||
};
|
||||
|
||||
class DispatcherBrowser: RscText {
|
||||
type = 106;
|
||||
idc = 1006;
|
||||
x = "safeZoneX + (safeZoneW * 0.1)";
|
||||
y = "safeZoneY + (safeZoneH * 0.1) + 0.10372";
|
||||
w = "safeZoneW * 0.8";
|
||||
h = "(safeZoneH * 0.8) - 0.10372 - 0.0556";
|
||||
colorBackground[] = {0, 0, 0, 0};
|
||||
};
|
||||
};
|
||||
};
|
||||
1
arma/client/addons/cad/ui/_site/bottombar.html
Normal file
1
arma/client/addons/cad/ui/_site/bottombar.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><span class="footer-brand">CAD Systems by IDS</span> <span class="footer-version">v1.0.0</span><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,o)=>e.then(()=>o.endsWith(".css")?this.loadCSS(o):o.endsWith(".js")?this.loadJS(o):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.js"]).catch(e=>console.error("[BOTTOMBAR] Load error:",e))</script></body></html>
|
||||
1
arma/client/addons/cad/ui/_site/cad-bottombar.css
Normal file
1
arma/client/addons/cad/ui/_site/cad-bottombar.css
Normal file
@ -0,0 +1 @@
|
||||
body{-webkit-backdrop-filter:blur(18px);background:linear-gradient(90deg,#0e131bf5,#121720ed 55%,#0d1219f5);border-top:1px solid #ffffff24;justify-content:space-between;align-items:center;min-height:36px;padding:0 20px;display:flex;position:absolute;bottom:0;left:0;right:0;overflow:hidden;box-shadow:0 -12px 26px #0000003d}.footer-brand,.footer-version{color:#f5f8ffcc;text-shadow:0 1px 10px #00000047;font-size:12px}.footer-brand{color:var(--accent);letter-spacing:.08em;text-transform:uppercase;font-weight:600}.footer-version{color:#f5f8ff9e}
|
||||
1
arma/client/addons/cad/ui/_site/cad-bottombar.js
Normal file
1
arma/client/addons/cad/ui/_site/cad-bottombar.js
Normal file
@ -0,0 +1 @@
|
||||
window.CADBottombar=window.CADBottombar||{init:()=>!0},window.CADBottombar.init();
|
||||
1
arma/client/addons/cad/ui/_site/cad-common.css
Normal file
1
arma/client/addons/cad/ui/_site/cad-common.css
Normal file
@ -0,0 +1 @@
|
||||
:root{--bg:#090c12d1;--panel:#141821e6;--panel2:#11151ed1;--stroke:#ffffff1f;--stroke2:#fff3;--text:#f5f8ffeb;--muted:#f5f8ff9e;--muted2:#f5f8ff6b;--accent:#68c4fff2;--danger:#ff6060f2;--shadow:0 20px 60px #0000008c;--radius:14px;--radius2:10px;--font:ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif}*{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--font);color:var(--text);background:var(--bg);-webkit-backdrop-filter:blur(16px)}.btn{border-radius:var(--radius2);color:var(--text);cursor:pointer;user-select:none;background:#ffffff08;border:1px solid #ffffff1a;padding:8px 16px;font-size:14px;transition:background .16s,border-color .16s,transform .16s}.btn:hover{background:#ffffff12;border-color:#ffffff29}.btn:active{transform:scale(.98)}.btn-close{color:#ffdcdcf2;background:#ff60601a;border-color:#ff606040;font-weight:700}.btn-close:hover{background:#ff606033;border-color:#ff606059}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-thumb{background:#ffffff1a;border:2px solid #0000001a;border-radius:999px}
|
||||
1
arma/client/addons/cad/ui/_site/cad-dispatcher.css
Normal file
1
arma/client/addons/cad/ui/_site/cad-dispatcher.css
Normal file
File diff suppressed because one or more lines are too long
1
arma/client/addons/cad/ui/_site/cad-dispatcher.js
Normal file
1
arma/client/addons/cad/ui/_site/cad-dispatcher.js
Normal file
File diff suppressed because one or more lines are too long
1
arma/client/addons/cad/ui/_site/cad-shared.js
Normal file
1
arma/client/addons/cad/ui/_site/cad-shared.js
Normal file
@ -0,0 +1 @@
|
||||
window.mapUIState={layersPanelVisible:!0,sidePanelElement:null},window.mapUI={formatGridCoordinate:t=>Math.round(Number(t)||0).toString().padStart(4,"0"),formatPosition(t){const e=Array.isArray(t)?t:[0,0,0];return`X: ${this.formatGridCoordinate(e[0])} Y: ${this.formatGridCoordinate(e[1])}`},sendEvent(t,e){A3API.SendAlert(JSON.stringify({event:t,data:e}))},updateCoordinates(t,e){const n=document.getElementById("coordsDisplay");n&&(n.textContent=this.formatPosition([t,e,0]))},updateScale(t){const e=document.getElementById("scaleDisplay");e&&(e.textContent=`Scale: 1:${Math.round(t)}`)},updateStatus(t){const e=document.getElementById("statusText");e&&(e.textContent=t)}},window.updateCoordinates=window.mapUI.updateCoordinates,window.updateScale=window.mapUI.updateScale,window.updateStatus=window.mapUI.updateStatus,window.ForgeBridge=window.ForgeBridge||{_handlers:{},on(t,e){this._handlers[t]=this._handlers[t]||[],this._handlers[t].push(e)},ready:t=>(window.mapUI.sendEvent("cad::ready",t||{}),!0),receive(t){if(!t||"object"!=typeof t)return;(this._handlers[t.event]||[]).forEach(e=>e(t.data||{}))},send:(t,e)=>(window.mapUI.sendEvent(t,e||{}),!0),close:t=>(window.mapUI.sendEvent("map::close",t||{}),!0)};
|
||||
1
arma/client/addons/cad/ui/_site/cad-sidepanel.css
Normal file
1
arma/client/addons/cad/ui/_site/cad-sidepanel.css
Normal file
File diff suppressed because one or more lines are too long
1
arma/client/addons/cad/ui/_site/cad-sidepanel.js
Normal file
1
arma/client/addons/cad/ui/_site/cad-sidepanel.js
Normal file
File diff suppressed because one or more lines are too long
1
arma/client/addons/cad/ui/_site/cad-topbar.css
Normal file
1
arma/client/addons/cad/ui/_site/cad-topbar.css
Normal file
@ -0,0 +1 @@
|
||||
body{background:0 0;grid-template-columns:auto minmax(0,1fr) auto auto auto;align-items:center;column-gap:16px;height:60px;padding:0 16px;display:grid;position:absolute;top:0;left:0;right:0;overflow:visible}body[data-mode=operations]{grid-template-columns:auto minmax(0,1fr) auto auto}body[data-mode=dispatch]{grid-template-columns:auto minmax(0,1fr) auto auto auto}body:before{content:"";height:60px;box-shadow:none;-webkit-backdrop-filter:blur(18px);z-index:0;pointer-events:none;background:linear-gradient(90deg,#10161ff5,#131a24f0 55%,#0f141cf5);border-bottom:none;position:absolute;inset:0 0 auto}body>*{z-index:1;position:relative}.logo{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;text-shadow:0 1px 12px #00000059;font-size:15px;font-weight:650}.header-main{align-items:center;gap:12px;min-width:0;display:flex}.title-block{flex-direction:column;flex:none;gap:1px;min-width:0;display:flex}.title-kicker{color:#dae3ec8f;text-transform:uppercase;letter-spacing:.12em;font-size:10px}.title-main{color:#f5f8ffeb;font-size:15px;font-weight:600}.operator-strip{flex:auto;align-items:center;gap:8px;min-width:0;display:flex}.operator-strip.is-hidden,.operator-controls.is-hidden{display:none}.operator-info{flex-direction:column;gap:0;min-width:88px;display:flex}.operator-label{color:#dae3ec80;text-transform:uppercase;letter-spacing:.12em;font-size:9px}.operator-info strong{color:#f5f8ffe6;font-size:12px;font-weight:550}.operator-controls{align-items:center;gap:6px;min-width:0;display:flex}.operator-select{min-width:92px;max-width:112px;color:var(--text);background:#0e141cf5;border:1px solid #ffffff24;padding:5px 8px;font-size:11px}.btn-operator{text-transform:uppercase;letter-spacing:.08em;min-width:84px;font-size:10px}.mode-controls{justify-self:end;align-items:center;gap:8px;display:flex}.mode-controls.is-hidden{display:none}.dispatch-view-controls{justify-self:end;align-items:center;gap:6px;display:flex}.dispatch-view-controls.is-hidden{display:none}.controls{justify-self:end;align-items:center;gap:8px;display:flex}.mode-text{color:#e9f1f8b8;text-transform:uppercase;letter-spacing:.1em;font-size:10px}.mode-switch{align-items:center;width:54px;height:28px;display:inline-flex;position:relative}.mode-switch input{opacity:0;pointer-events:none;position:absolute}.mode-slider{background:#161d27eb;border:1px solid #ffffff24;border-radius:999px;width:54px;height:28px;transition:border-color .16s,background .16s;position:relative;box-shadow:inset 0 1px 10px #00000038}.mode-slider:after{content:"";background:linear-gradient(#edf4fbfa,#bdcdddeb);border-radius:50%;width:20px;height:20px;transition:transform .16s,background .16s;position:absolute;top:3px;left:3px;box-shadow:0 4px 12px #00000042}.mode-switch input:checked+.mode-slider{background:#0e2538f2;border-color:#5bbbff6b}.mode-switch input:checked+.mode-slider:after{background:linear-gradient(#83d4fffa,#48aae7f0);transform:translate(26px)}.btn-close{min-width:42px}.btn-dispatch-view{text-transform:uppercase;letter-spacing:.08em;min-width:66px;padding:6px 10px;font-size:10px}.btn-icon{justify-content:center;align-items:center;width:34px;min-width:34px;height:30px;padding:0;font-size:16px;line-height:1;display:inline-flex}.btn-refresh{width:40px;min-width:40px;font-size:17px;font-weight:600}.btn-dispatch-view.is-active{color:var(--accent);background:#0f283af5;border-color:#5bbbff6b}.btn-close{font-size:14px}body{pointer-events:none}body .logo,body .title-block,body .operator-strip,body .operator-controls,body .mode-controls,body .dispatch-view-controls,body .controls,body .mode-switch,body .mode-switch *,body button,body select,body label{pointer-events:auto}
|
||||
1
arma/client/addons/cad/ui/_site/cad-topbar.js
Normal file
1
arma/client/addons/cad/ui/_site/cad-topbar.js
Normal file
@ -0,0 +1 @@
|
||||
window.cadTopbar={mode:"operations",dispatchView:"board",currentGroup:null,session:{},init(){document.getElementById("btnClose").addEventListener("click",()=>{window.mapUI.sendEvent("map::close",null)}),document.getElementById("modeToggle").addEventListener("change",e=>{window.mapUI.sendEvent("cad::mode::set",{mode:e.target.checked?"dispatch":"operations"})}),document.getElementById("dispatchRefreshBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::refresh",{})}),document.getElementById("dispatchBoardBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::dispatchView::set",{dispatchView:"board"})}),document.getElementById("dispatchMapBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::dispatchView::set",{dispatchView:"map"})}),document.getElementById("operatorRoleBtn").addEventListener("click",()=>{this.currentGroup&&window.mapUI.sendEvent("cad::groups::role",{groupID:this.currentGroup.groupId||"",role:document.getElementById("operatorRoleSelect").value})}),document.getElementById("operatorStatusBtn").addEventListener("click",()=>{this.currentGroup&&window.mapUI.sendEvent("cad::groups::status",{groupID:this.currentGroup.groupId||"",status:document.getElementById("operatorStatusSelect").value})}),window.mapUI.sendEvent("cad::topbar::ready",{})},formatLocation(e){const t=Array.isArray(e?.position)?e.position:[0,0,0];return window.mapUI.formatPosition(t)},receiveState(e){this.session=e&&e.session&&"object"==typeof e.session?e.session:{},this.mode=e&&"string"==typeof e.mode?e.mode:"operations",this.dispatchView=e&&"string"==typeof e.dispatchView?e.dispatchView:"board",this.currentGroup=e&&e.currentGroup&&"object"==typeof e.currentGroup?e.currentGroup:null;const t=document.getElementById("modeControls"),o=!!this.session.isDispatcher,s=!(!this.currentGroup||!this.session.isLeader&&!this.session.isDispatcher),n=document.getElementById("operatorStrip"),d=document.getElementById("operatorControls"),i=document.getElementById("dispatchViewControls"),r=document.getElementById("dispatchRefreshBtn"),a=document.getElementById("dispatchBoardBtn"),c=document.getElementById("dispatchMapBtn");t.classList.toggle("is-hidden",!o),i.classList.toggle("is-hidden",!o||"dispatch"!==this.mode),n.classList.toggle("is-hidden","operations"!==this.mode||!this.currentGroup),d.classList.toggle("is-hidden",!s),document.body.dataset.mode=this.mode,document.body.dataset.dispatcher=o?"true":"false",document.getElementById("modeToggle").checked="dispatch"===this.mode,a.classList.toggle("is-active","board"===this.dispatchView),c.classList.toggle("is-active","map"===this.dispatchView),r.title="dispatch"===this.mode?"Refresh dispatch board":"Refresh CAD",r.setAttribute("aria-label","dispatch"===this.mode?"Refresh dispatch board":"Refresh CAD"),document.getElementById("operatorGroupName").textContent=this.currentGroup?this.currentGroup.callsign||this.currentGroup.groupId||"Current Group":"No Group",document.getElementById("operatorLocation").textContent=this.currentGroup?this.formatLocation(this.currentGroup):"Unavailable",this.currentGroup&&(document.getElementById("operatorRoleSelect").value=this.currentGroup.role||"infantry",document.getElementById("operatorStatusSelect").value=this.currentGroup.status||"available")}},window.cadTopbar.init();
|
||||
1
arma/client/addons/cad/ui/_site/dispatcher.html
Normal file
1
arma/client/addons/cad/ui/_site/dispatcher.html
Normal file
File diff suppressed because one or more lines are too long
1
arma/client/addons/cad/ui/_site/sidepanel.html
Normal file
1
arma/client/addons/cad/ui/_site/sidepanel.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="panel-header"><h3>CAD System</h3></div><div class="panel-content"><div id="cadStatusMessage" class="task-status-message"></div><div id="cadDangerAlert" class="cad-danger-alert is-hidden"></div><div id="cadRequestAlert" class="cad-warning-alert is-hidden"></div><div class="cad-tabs" role="tablist" aria-label="CAD Sections"><button id="tabContractsBtn" class="cad-tab is-active" type="button" data-tab="contracts">Contracts</button> <button id="tabRosterBtn" class="cad-tab" type="button" data-tab="roster">Roster</button> <button id="tabRequestsBtn" class="cad-tab" type="button" data-tab="requests">Requests</button> <button id="tabActivityBtn" class="cad-tab" type="button" data-tab="activity">Activity</button></div><div class="cad-tab-panels"><div id="contractsPanel" class="cad-section is-active" data-panel="contracts"><div class="cad-section-header">Contracts</div><div id="taskList" class="task-list"><div class="placeholder-message"><p>Loading contracts...</p></div></div></div><div id="rosterPanel" class="cad-section" data-panel="roster"><div class="cad-section-header">Roster</div><div id="rosterList" class="task-list"><div class="placeholder-message"><p>Loading roster...</p></div></div></div><div id="requestsPanel" class="cad-section" data-panel="requests"><div class="cad-section-header">Support Requests</div><div id="requestList" class="task-list"><div class="placeholder-message"><p>No support requests.</p></div></div></div><div id="activityPanel" class="cad-section" data-panel="activity"><div class="cad-section-header">Activity</div><div id="activityList" class="task-list"><div class="placeholder-message"><p>No recent activity.</p></div></div></div></div></div><div id="cadRequestModal" class="cad-modal is-hidden"><div class="cad-modal-backdrop"></div><div class="cad-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="cadRequestModalTitle"><div class="cad-modal-header"><div><div class="cad-section-header">Support Request</div><h3 id="cadRequestModalTitle">Submit Request</h3></div><button id="cadRequestModalCloseBtn" class="cad-icon-btn" type="button" aria-label="Close support request form">x</button></div><div class="cad-modal-body"><div class="cad-modal-fields"><label class="cad-field"><span>Priority</span> <select id="cadRequestPrioritySelect" class="cad-select"><option value="routine">routine</option><option value="priority" selected>priority</option><option value="emergency">emergency</option></select></label><div id="cadRequestFields" class="cad-modal-fields"></div></div></div><div class="cad-modal-actions"><button id="cadRequestModalSaveBtn" type="button" class="task-accept-btn">Submit Request</button></div></div></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const d=document.createElement("style");d.textContent=e,document.head.appendChild(d)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,d)=>e.then(()=>d.endsWith(".css")?this.loadCSS(d):d.endsWith(".js")?this.loadJS(d):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.js"]).catch(e=>console.error("[SIDEPANEL] Load error:",e))</script></body></html>
|
||||
1
arma/client/addons/cad/ui/_site/topbar.html
Normal file
1
arma/client/addons/cad/ui/_site/topbar.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"></head><body><div class="logo">FORGE OS</div><div class="header-main"><div class="title-block"><span class="title-kicker">Cad Systems</span> <strong class="title-main">FORGE Command & Dispatch</strong></div><div id="operatorStrip" class="operator-strip is-hidden"><div class="operator-info"><span class="operator-label">Current Group</span> <strong id="operatorGroupName">No Group</strong></div><div class="operator-info"><span class="operator-label">Location</span> <strong id="operatorLocation">Unavailable</strong></div><div id="operatorControls" class="operator-controls is-hidden"><select id="operatorRoleSelect" class="operator-select"><option value="infantry">infantry</option><option value="recon">recon</option><option value="armor">armor</option><option value="air">air</option><option value="logistics">logistics</option><option value="support">support</option></select> <button id="operatorRoleBtn" class="btn btn-operator" type="button">Update Role</button> <select id="operatorStatusSelect" class="operator-select"><option value="available">available</option><option value="en_route">en route</option><option value="on_task">on task</option><option value="holding">holding</option><option value="danger">danger</option><option value="unavailable">unavailable</option></select> <button id="operatorStatusBtn" class="btn btn-operator" type="button">Update Status</button></div></div></div><div id="modeControls" class="mode-controls is-hidden"><span class="mode-text">Ops</span> <label class="mode-switch" for="modeToggle"><input id="modeToggle" type="checkbox"> <span class="mode-slider"></span></label> <span class="mode-text">Dispatch</span></div><div id="dispatchViewControls" class="dispatch-view-controls is-hidden"><button id="dispatchBoardBtn" class="btn btn-dispatch-view is-active" type="button">Board</button> <button id="dispatchMapBtn" class="btn btn-dispatch-view" type="button">Map</button></div><div class="controls"><button id="dispatchRefreshBtn" class="btn btn-icon btn-refresh" type="button" aria-label="Refresh board" title="Refresh board">↻</button> <button id="btnClose" class="btn btn-icon btn-close">X</button></div><script>window.MapLoader={loadCSS:e=>A3API.RequestFile(e).then(e=>{const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}),loadJS(path){return A3API.RequestFile(path).then(js=>{eval(js)})},loadAll(e){return e.reduce((e,o)=>e.then(()=>o.endsWith(".css")?this.loadCSS(o):o.endsWith(".js")?this.loadJS(o):Promise.resolve()),Promise.resolve())}},MapLoader.loadAll(["forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.css","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js","forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.js"]).catch(e=>console.error("[TOPBAR] Load error:",e))</script></body></html>
|
||||
49
arma/client/addons/cad/ui/src/bottombar.html
Normal file
49
arma/client/addons/cad/ui/src/bottombar.html
Normal file
@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<span class="footer-brand">CAD Systems by IDS</span>
|
||||
<span class="footer-version">v1.0.0</span>
|
||||
|
||||
<script>
|
||||
window.MapLoader = {
|
||||
loadCSS(path) {
|
||||
return A3API.RequestFile(path).then((css) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
},
|
||||
loadJS(path) {
|
||||
return A3API.RequestFile(path).then((js) => {
|
||||
eval(js);
|
||||
});
|
||||
},
|
||||
loadAll(resources) {
|
||||
return resources.reduce((promise, resource) => {
|
||||
return promise.then(() => {
|
||||
if (resource.endsWith(".css")) {
|
||||
return this.loadCSS(resource);
|
||||
}
|
||||
|
||||
if (resource.endsWith(".js")) {
|
||||
return this.loadJS(resource);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}, Promise.resolve());
|
||||
},
|
||||
};
|
||||
|
||||
MapLoader.loadAll([
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-bottombar.js",
|
||||
]).catch((err) => console.error("[BOTTOMBAR] Load error:", err));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
7
arma/client/addons/cad/ui/src/bottombar.js
Normal file
7
arma/client/addons/cad/ui/src/bottombar.js
Normal file
@ -0,0 +1,7 @@
|
||||
window.CADBottombar = window.CADBottombar || {
|
||||
init() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
window.CADBottombar.init();
|
||||
372
arma/client/addons/cad/ui/src/dispatcher.html
Normal file
372
arma/client/addons/cad/ui/src/dispatcher.html
Normal file
@ -0,0 +1,372 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="dispatch-shell">
|
||||
<header class="dispatch-header">
|
||||
<div>
|
||||
<p class="dispatch-kicker">Dispatch Dashboard</p>
|
||||
<h2>Operational Board</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="dispatcherStatusMessage" class="dispatch-status"></div>
|
||||
<div
|
||||
id="dispatcherDangerAlert"
|
||||
class="dispatch-danger-alert is-hidden"
|
||||
></div>
|
||||
<div
|
||||
id="dispatcherRequestAlert"
|
||||
class="dispatch-warning-alert is-hidden"
|
||||
></div>
|
||||
|
||||
<section class="dispatch-metrics">
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Open Contracts</span>
|
||||
<strong id="metricOpenContracts">0</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Assigned Contracts</span>
|
||||
<strong id="metricAssignedContracts">0</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">Active Groups</span>
|
||||
<strong id="metricActiveGroups">0</strong>
|
||||
</div>
|
||||
<div id="metricOpenRequestsCard" class="metric-card">
|
||||
<span class="metric-label">Open Requests</span>
|
||||
<strong id="metricOpenRequests">0</strong>
|
||||
</div>
|
||||
<div id="metricDangerGroupsCard" class="metric-card">
|
||||
<span class="metric-label">Groups In Danger</span>
|
||||
<strong id="metricDangerGroups">0</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dispatch-grid">
|
||||
<section class="dispatch-panel dispatch-panel-open">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Available Contracts</h3>
|
||||
<button
|
||||
id="dispatcherCreateOrderBtn"
|
||||
type="button"
|
||||
class="dispatch-icon-btn"
|
||||
aria-label="Create dispatch order"
|
||||
title="Create dispatch order"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="dispatcherOpenContracts"
|
||||
class="dispatch-list"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<section class="dispatch-panel dispatch-panel-assigned">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Assigned Contracts</h3>
|
||||
</div>
|
||||
<div
|
||||
id="dispatcherAssignedContracts"
|
||||
class="dispatch-list"
|
||||
></div>
|
||||
</section>
|
||||
|
||||
<section class="dispatch-panel dispatch-panel-groups">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Group Board</h3>
|
||||
</div>
|
||||
<div id="dispatcherGroups" class="dispatch-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="dispatch-panel dispatch-panel-activity">
|
||||
<div class="dispatch-panel-header">
|
||||
<h3>Requests & Activity</h3>
|
||||
</div>
|
||||
<div id="dispatcherActivity" class="dispatch-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="dispatcherGroupModal" class="dispatch-modal is-hidden">
|
||||
<div class="dispatch-modal-backdrop"></div>
|
||||
<div
|
||||
class="dispatch-modal-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dispatcherGroupModalTitle"
|
||||
>
|
||||
<div class="dispatch-modal-header">
|
||||
<div>
|
||||
<p class="dispatch-kicker">Group Editor</p>
|
||||
<h3 id="dispatcherGroupModalTitle">Manage Group</h3>
|
||||
</div>
|
||||
<button
|
||||
id="dispatcherGroupModalCloseBtn"
|
||||
class="dispatch-icon-btn"
|
||||
type="button"
|
||||
aria-label="Close group editor"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<div class="dispatch-modal-body">
|
||||
<div class="dispatch-meta-grid">
|
||||
<div>
|
||||
<span class="metric-label">Callsign</span>
|
||||
<strong id="dispatcherModalGroupCallsign"
|
||||
>-</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Leader</span>
|
||||
<strong id="dispatcherModalGroupLeader"
|
||||
>-</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Current Task</span>
|
||||
<strong id="dispatcherModalGroupTask"
|
||||
>None</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Org</span>
|
||||
<strong id="dispatcherModalGroupOrg"
|
||||
>default</strong
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispatch-modal-fields">
|
||||
<label class="dispatch-field">
|
||||
<span>Role</span>
|
||||
<select
|
||||
id="dispatcherModalRoleSelect"
|
||||
class="dispatch-select"
|
||||
></select>
|
||||
</label>
|
||||
<label class="dispatch-field">
|
||||
<span>Status</span>
|
||||
<select
|
||||
id="dispatcherModalStatusSelect"
|
||||
class="dispatch-select"
|
||||
></select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispatch-modal-actions">
|
||||
<button
|
||||
id="dispatcherGroupModalSaveBtn"
|
||||
type="button"
|
||||
class="dispatch-btn"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dispatcherOrderModal" class="dispatch-modal is-hidden">
|
||||
<div class="dispatch-modal-backdrop"></div>
|
||||
<div
|
||||
class="dispatch-modal-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dispatcherOrderModalTitle"
|
||||
>
|
||||
<div class="dispatch-modal-header">
|
||||
<div>
|
||||
<p class="dispatch-kicker">Dispatch Order</p>
|
||||
<h3 id="dispatcherOrderModalTitle">
|
||||
Create Support Order
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
id="dispatcherOrderModalCloseBtn"
|
||||
class="dispatch-icon-btn"
|
||||
type="button"
|
||||
aria-label="Close dispatch order editor"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<div class="dispatch-modal-body">
|
||||
<div class="dispatch-modal-fields">
|
||||
<label class="dispatch-field">
|
||||
<span>Assignee Group</span>
|
||||
<select
|
||||
id="dispatcherOrderAssigneeSelect"
|
||||
class="dispatch-select"
|
||||
></select>
|
||||
</label>
|
||||
<label class="dispatch-field">
|
||||
<span>Target Group</span>
|
||||
<select
|
||||
id="dispatcherOrderTargetSelect"
|
||||
class="dispatch-select"
|
||||
></select>
|
||||
</label>
|
||||
<label class="dispatch-field">
|
||||
<span>Priority</span>
|
||||
<select
|
||||
id="dispatcherOrderPrioritySelect"
|
||||
class="dispatch-select"
|
||||
>
|
||||
<option value="routine">routine</option>
|
||||
<option value="priority" selected>
|
||||
priority
|
||||
</option>
|
||||
<option value="emergency">emergency</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="dispatch-field">
|
||||
<span>Order Note</span>
|
||||
<textarea
|
||||
id="dispatcherOrderNoteInput"
|
||||
class="dispatch-textarea"
|
||||
rows="4"
|
||||
placeholder="Optional order note for the assigned group."
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispatch-modal-actions">
|
||||
<button
|
||||
id="dispatcherOrderModalSaveBtn"
|
||||
type="button"
|
||||
class="dispatch-btn"
|
||||
>
|
||||
Create Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dispatcherRequestModal" class="dispatch-modal is-hidden">
|
||||
<div class="dispatch-modal-backdrop"></div>
|
||||
<div
|
||||
class="dispatch-modal-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dispatcherRequestModalTitle"
|
||||
>
|
||||
<div class="dispatch-modal-header">
|
||||
<div>
|
||||
<p class="dispatch-kicker">Support Request</p>
|
||||
<h3 id="dispatcherRequestModalTitle">
|
||||
Request Details
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
id="dispatcherRequestModalCloseBtn"
|
||||
class="dispatch-icon-btn"
|
||||
type="button"
|
||||
aria-label="Close support request details"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<div class="dispatch-modal-body">
|
||||
<div class="dispatch-meta-grid">
|
||||
<div>
|
||||
<span class="metric-label">Title</span>
|
||||
<strong id="dispatcherRequestTitle"
|
||||
>Support Request</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Priority</span>
|
||||
<strong id="dispatcherRequestPriority"
|
||||
>priority</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Group</span>
|
||||
<strong id="dispatcherRequestGroup"
|
||||
>Unknown</strong
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="metric-label">Type</span>
|
||||
<strong id="dispatcherRequestType"
|
||||
>request</strong
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispatch-field">
|
||||
<span>Summary</span>
|
||||
<div
|
||||
id="dispatcherRequestSummary"
|
||||
class="dispatch-detail-block"
|
||||
></div>
|
||||
</div>
|
||||
<div class="dispatch-field">
|
||||
<span>Submitted Fields</span>
|
||||
<div
|
||||
id="dispatcherRequestFields"
|
||||
class="dispatch-detail-list"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dispatch-modal-actions">
|
||||
<button
|
||||
id="dispatcherRequestConvertBtn"
|
||||
type="button"
|
||||
class="dispatch-btn dispatch-btn-secondary"
|
||||
>
|
||||
Convert to Order
|
||||
</button>
|
||||
<button
|
||||
id="dispatcherRequestModalDoneBtn"
|
||||
type="button"
|
||||
class="dispatch-btn"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.MapLoader = {
|
||||
loadCSS(path) {
|
||||
return A3API.RequestFile(path).then((css) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
},
|
||||
loadJS(path) {
|
||||
return A3API.RequestFile(path).then((js) => {
|
||||
eval(js);
|
||||
});
|
||||
},
|
||||
loadAll(resources) {
|
||||
return resources.reduce((promise, resource) => {
|
||||
return promise.then(() => {
|
||||
if (resource.endsWith(".css")) {
|
||||
return this.loadCSS(resource);
|
||||
}
|
||||
|
||||
if (resource.endsWith(".js")) {
|
||||
return this.loadJS(resource);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}, Promise.resolve());
|
||||
},
|
||||
};
|
||||
|
||||
MapLoader.loadAll([
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-dispatcher.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-dispatcher.js",
|
||||
]).catch((err) => console.error("[DISPATCHER] Load error:", err));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
120
arma/client/addons/cad/ui/src/dispatcher/formatters.js
Normal file
120
arma/client/addons/cad/ui/src/dispatcher/formatters.js
Normal file
@ -0,0 +1,120 @@
|
||||
window.cadDispatcherFormatters = {
|
||||
getDangerGroups() {
|
||||
return this.groups.filter((group) => (group.status || "") === "danger");
|
||||
},
|
||||
getSupportAlertRequests() {
|
||||
return this.requests.filter((request) =>
|
||||
["medevac_9line", "fire_support", "air_support"].includes(
|
||||
request.type || "",
|
||||
),
|
||||
);
|
||||
},
|
||||
buildSupportAlertMessage() {
|
||||
const alertRequests = this.getSupportAlertRequests();
|
||||
if (!alertRequests.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const labels = alertRequests.map((request) => {
|
||||
const groupLabel =
|
||||
request.groupCallsign || request.groupId || "Unknown Group";
|
||||
const typeLabel = this.getRequestTypeLabel(
|
||||
request.type || "request",
|
||||
);
|
||||
return `${groupLabel} ${typeLabel}`;
|
||||
});
|
||||
|
||||
return `Support request alert: ${labels.join(", ")}`;
|
||||
},
|
||||
getSortedGroups() {
|
||||
return this.groups.slice().sort((left, right) => {
|
||||
const leftDanger = (left.status || "") === "danger" ? 0 : 1;
|
||||
const rightDanger = (right.status || "") === "danger" ? 0 : 1;
|
||||
|
||||
if (leftDanger !== rightDanger) {
|
||||
return leftDanger - rightDanger;
|
||||
}
|
||||
|
||||
const leftCallsign = left.callsign || left.groupId || "";
|
||||
const rightCallsign = right.callsign || right.groupId || "";
|
||||
return leftCallsign.localeCompare(rightCallsign);
|
||||
});
|
||||
},
|
||||
isDispatchOrder(entry) {
|
||||
return (
|
||||
!!entry.isDispatchOrder || (entry.type || "") === "dispatch_order"
|
||||
);
|
||||
},
|
||||
formatTypeLabel(entry) {
|
||||
const typeLabel = (entry.type || "task").replaceAll("_", " ");
|
||||
return this.isDispatchOrder(entry) ? "dispatch order" : typeLabel;
|
||||
},
|
||||
getRequestTypeLabel(typeID) {
|
||||
switch (typeID) {
|
||||
case "medevac_9line":
|
||||
return "9-Line MEDEVAC";
|
||||
case "ace_lace":
|
||||
return "ACE/LACE";
|
||||
case "fire_support":
|
||||
return "Fire Support";
|
||||
case "air_support":
|
||||
return "Air Support";
|
||||
case "logreq":
|
||||
return "LOGREQ";
|
||||
default:
|
||||
return (typeID || "request").replaceAll("_", " ");
|
||||
}
|
||||
},
|
||||
buildGroupOptions(selectedGroupID) {
|
||||
return this.getSortedGroups()
|
||||
.map((group) => {
|
||||
const groupID = group.groupId || "";
|
||||
return `<option value="${groupID}" ${groupID === selectedGroupID ? "selected" : ""}>${group.callsign || groupID}</option>`;
|
||||
})
|
||||
.join("");
|
||||
},
|
||||
formatRequestFieldLabel(fieldID) {
|
||||
return (fieldID || "field")
|
||||
.replaceAll("_", " ")
|
||||
.replace(/\b\w/g, (character) => character.toUpperCase());
|
||||
},
|
||||
formatRequestFieldValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(", ");
|
||||
}
|
||||
|
||||
if (value && typeof value === "object") {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
const text = String(value ?? "").trim();
|
||||
return text || "Not provided";
|
||||
},
|
||||
buildRequestOrderNote(request) {
|
||||
const typeLabel = this.getRequestTypeLabel(request.type || "request");
|
||||
const groupLabel =
|
||||
request.groupCallsign || request.groupId || "Unknown Group";
|
||||
const summary = (request.summary || "").trim();
|
||||
const fieldDetails =
|
||||
request.fields && typeof request.fields === "object"
|
||||
? Object.entries(request.fields)
|
||||
.map(([fieldID, value]) => {
|
||||
const fieldValue =
|
||||
this.formatRequestFieldValue(value);
|
||||
if (fieldValue === "Not provided") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${this.formatRequestFieldLabel(fieldID)} ${fieldValue}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const details = fieldDetails.length
|
||||
? fieldDetails
|
||||
: [summary].filter(Boolean);
|
||||
|
||||
return details.length
|
||||
? `${typeLabel} requested by ${groupLabel}. ${details.join(" | ")}`
|
||||
: `${typeLabel} requested by ${groupLabel}.`;
|
||||
},
|
||||
};
|
||||
274
arma/client/addons/cad/ui/src/dispatcher/index.js
Normal file
274
arma/client/addons/cad/ui/src/dispatcher/index.js
Normal file
@ -0,0 +1,274 @@
|
||||
const dispatcherFormatters = window.cadDispatcherFormatters || {};
|
||||
const dispatcherModals = window.cadDispatcherModals || {};
|
||||
const dispatcherRender = window.cadDispatcherRender || {};
|
||||
|
||||
window.cadDispatcher = {
|
||||
contracts: [],
|
||||
requests: [],
|
||||
groups: [],
|
||||
activity: [],
|
||||
session: {},
|
||||
editingGroupId: "",
|
||||
viewingRequestId: "",
|
||||
convertingRequestId: "",
|
||||
statuses: [
|
||||
"available",
|
||||
"en_route",
|
||||
"on_task",
|
||||
"holding",
|
||||
"danger",
|
||||
"unavailable",
|
||||
],
|
||||
roles: ["infantry", "recon", "armor", "air", "logistics", "support"],
|
||||
...dispatcherFormatters,
|
||||
...dispatcherModals,
|
||||
...dispatcherRender,
|
||||
init() {
|
||||
document
|
||||
.getElementById("dispatcherCreateOrderBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.openOrderModal();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatcherGroupModalCloseBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.closeGroupModal();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatcherGroupModalSaveBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.applyGroupUpdates();
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop")
|
||||
.addEventListener("click", () => {
|
||||
this.closeGroupModal();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatcherOrderModalCloseBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.closeOrderModal();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatcherOrderModalSaveBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.createDispatchOrder();
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector("#dispatcherOrderModal .dispatch-modal-backdrop")
|
||||
.addEventListener("click", () => {
|
||||
this.closeOrderModal();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatcherRequestModalCloseBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.closeRequestModal();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatcherRequestModalDoneBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.closeRequestModal();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatcherRequestConvertBtn")
|
||||
.addEventListener("click", () => {
|
||||
this.convertViewedRequestToOrder();
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector("#dispatcherRequestModal .dispatch-modal-backdrop")
|
||||
.addEventListener("click", () => {
|
||||
this.closeRequestModal();
|
||||
});
|
||||
|
||||
window.mapUI.sendEvent("cad::dispatcher::ready", {});
|
||||
},
|
||||
receiveHydrate(payload) {
|
||||
this.contracts = Array.isArray(payload.contracts)
|
||||
? payload.contracts
|
||||
: [];
|
||||
this.requests = Array.isArray(payload.requests) ? payload.requests : [];
|
||||
this.groups = Array.isArray(payload.groups) ? payload.groups : [];
|
||||
this.activity = Array.isArray(payload.activity) ? payload.activity : [];
|
||||
this.session =
|
||||
payload.session && typeof payload.session === "object"
|
||||
? payload.session
|
||||
: {};
|
||||
|
||||
const statusEl = document.getElementById("dispatcherStatusMessage");
|
||||
if (
|
||||
statusEl &&
|
||||
(!statusEl.dataset.type || statusEl.dataset.type === "info")
|
||||
) {
|
||||
this.setStatus("", "");
|
||||
}
|
||||
|
||||
this.syncOpenModal();
|
||||
this.syncOrderModal();
|
||||
this.syncRequestModal();
|
||||
this.render();
|
||||
},
|
||||
setStatus(message, type) {
|
||||
const statusEl = document.getElementById("dispatcherStatusMessage");
|
||||
if (!statusEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.textContent = message || "";
|
||||
statusEl.dataset.type = type || "";
|
||||
},
|
||||
createDispatchOrder() {
|
||||
const assigneeGroupID = document.getElementById(
|
||||
"dispatcherOrderAssigneeSelect",
|
||||
).value;
|
||||
const targetGroupID = document.getElementById(
|
||||
"dispatcherOrderTargetSelect",
|
||||
).value;
|
||||
const priority = document.getElementById(
|
||||
"dispatcherOrderPrioritySelect",
|
||||
).value;
|
||||
const note = document.getElementById("dispatcherOrderNoteInput").value;
|
||||
const sourceRequest = this.convertingRequestId
|
||||
? this.requests.find(
|
||||
(entry) =>
|
||||
(entry.requestId || "") === this.convertingRequestId,
|
||||
) || null
|
||||
: null;
|
||||
|
||||
if (!assigneeGroupID || !targetGroupID) {
|
||||
this.setStatus(
|
||||
"Select both an assignee and a target group.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (assigneeGroupID === targetGroupID) {
|
||||
this.setStatus(
|
||||
"Assignee and target groups must be different.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus(
|
||||
this.convertingRequestId
|
||||
? "Creating dispatch order from request..."
|
||||
: "Creating dispatch order...",
|
||||
"info",
|
||||
);
|
||||
window.mapUI.sendEvent("cad::dispatchOrder::create", {
|
||||
assigneeGroupID: assigneeGroupID,
|
||||
targetGroupID: targetGroupID,
|
||||
note: note.trim(),
|
||||
priority: priority,
|
||||
request: sourceRequest
|
||||
? {
|
||||
requestId: sourceRequest.requestId || "",
|
||||
type: sourceRequest.type || "",
|
||||
title: sourceRequest.title || "",
|
||||
summary: sourceRequest.summary || "",
|
||||
fields:
|
||||
sourceRequest.fields &&
|
||||
typeof sourceRequest.fields === "object"
|
||||
? sourceRequest.fields
|
||||
: {},
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
this.closeOrderModal();
|
||||
},
|
||||
assignTask(taskID) {
|
||||
const selector = document.getElementById(
|
||||
`dispatcher-assign-group-${taskID}`,
|
||||
);
|
||||
if (!selector || !selector.value) {
|
||||
this.setStatus(
|
||||
"Select a group before assigning a contract.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus("Submitting assignment...", "info");
|
||||
window.mapUI.sendEvent("cad::tasks::assign", {
|
||||
taskID: taskID,
|
||||
groupID: selector.value,
|
||||
note: "",
|
||||
});
|
||||
},
|
||||
applyGroupUpdates() {
|
||||
if (!this.editingGroupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = this.groups.find(
|
||||
(entry) => entry.groupId === this.editingGroupId,
|
||||
);
|
||||
if (!group) {
|
||||
this.closeGroupModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const roleValue = document.getElementById(
|
||||
"dispatcherModalRoleSelect",
|
||||
).value;
|
||||
const statusValue = document.getElementById(
|
||||
"dispatcherModalStatusSelect",
|
||||
).value;
|
||||
const nextRole =
|
||||
roleValue && roleValue !== (group.role || "") ? roleValue : "";
|
||||
const nextStatus =
|
||||
statusValue && statusValue !== (group.status || "")
|
||||
? statusValue
|
||||
: "";
|
||||
const hasChanges = nextRole || nextStatus;
|
||||
|
||||
if (!hasChanges) {
|
||||
this.setStatus("No group changes to save.", "info");
|
||||
this.closeGroupModal();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus("Updating group profile...", "info");
|
||||
window.mapUI.sendEvent("cad::groups::profile", {
|
||||
groupID: this.editingGroupId,
|
||||
role: nextRole,
|
||||
status: nextStatus,
|
||||
});
|
||||
|
||||
this.closeGroupModal();
|
||||
},
|
||||
closeDispatchOrder(taskID) {
|
||||
if (!taskID) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus("Closing dispatch order...", "info");
|
||||
window.mapUI.sendEvent("cad::dispatchOrder::close", {
|
||||
taskID: taskID,
|
||||
});
|
||||
},
|
||||
closeSupportRequest(requestID) {
|
||||
if (!requestID) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStatus("Closing support request...", "info");
|
||||
window.mapUI.sendEvent("cad::supportRequest::close", {
|
||||
requestID: requestID,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
window.cadDispatcher.init();
|
||||
269
arma/client/addons/cad/ui/src/dispatcher/modals.js
Normal file
269
arma/client/addons/cad/ui/src/dispatcher/modals.js
Normal file
@ -0,0 +1,269 @@
|
||||
window.cadDispatcherModals = {
|
||||
openOrderModal() {
|
||||
this.convertingRequestId = "";
|
||||
this.populateOrderModal();
|
||||
document.getElementById("dispatcherOrderModalTitle").textContent =
|
||||
"Create Support Order";
|
||||
document
|
||||
.getElementById("dispatcherOrderModal")
|
||||
.classList.remove("is-hidden");
|
||||
},
|
||||
closeOrderModal() {
|
||||
this.convertingRequestId = "";
|
||||
document.getElementById("dispatcherOrderNoteInput").value = "";
|
||||
document.getElementById("dispatcherOrderPrioritySelect").value =
|
||||
"priority";
|
||||
document.getElementById("dispatcherOrderModalTitle").textContent =
|
||||
"Create Support Order";
|
||||
document
|
||||
.getElementById("dispatcherOrderModal")
|
||||
.classList.add("is-hidden");
|
||||
},
|
||||
openRequestModal(requestID) {
|
||||
const request = this.requests.find(
|
||||
(entry) => entry.requestId === requestID,
|
||||
);
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewingRequestId = requestID;
|
||||
this.populateRequestModal(request);
|
||||
document
|
||||
.getElementById("dispatcherRequestModal")
|
||||
.classList.remove("is-hidden");
|
||||
},
|
||||
closeRequestModal() {
|
||||
this.viewingRequestId = "";
|
||||
document
|
||||
.getElementById("dispatcherRequestModal")
|
||||
.classList.add("is-hidden");
|
||||
},
|
||||
syncRequestModal() {
|
||||
if (!this.viewingRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = this.requests.find(
|
||||
(entry) => entry.requestId === this.viewingRequestId,
|
||||
);
|
||||
if (!request) {
|
||||
this.closeRequestModal();
|
||||
return;
|
||||
}
|
||||
|
||||
this.populateRequestModal(request);
|
||||
},
|
||||
populateRequestModal(request) {
|
||||
const fields =
|
||||
request.fields && typeof request.fields === "object"
|
||||
? Object.entries(request.fields)
|
||||
: [];
|
||||
const fieldsHTML = fields.length
|
||||
? fields
|
||||
.map(
|
||||
([fieldID, value]) => `
|
||||
<div class="dispatch-detail-row">
|
||||
<span class="dispatch-detail-label">${this.formatRequestFieldLabel(fieldID)}</span>
|
||||
<span class="dispatch-detail-value">${this.formatRequestFieldValue(value)}</span>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
: '<div class="placeholder-message"><p>No submitted fields.</p></div>';
|
||||
|
||||
document.getElementById("dispatcherRequestTitle").textContent =
|
||||
request.title || request.requestId || "Support Request";
|
||||
document.getElementById("dispatcherRequestPriority").textContent = (
|
||||
request.priority || "priority"
|
||||
).replaceAll("_", " ");
|
||||
document.getElementById("dispatcherRequestGroup").textContent =
|
||||
request.groupCallsign || request.groupId || "Unknown";
|
||||
document.getElementById("dispatcherRequestType").textContent =
|
||||
this.getRequestTypeLabel(request.type || "request");
|
||||
document.getElementById("dispatcherRequestSummary").textContent =
|
||||
request.summary || "No summary provided.";
|
||||
document.getElementById("dispatcherRequestFields").innerHTML =
|
||||
fieldsHTML;
|
||||
},
|
||||
convertRequestToOrder(requestID) {
|
||||
const request = this.requests.find(
|
||||
(entry) => (entry.requestId || "") === requestID,
|
||||
);
|
||||
if (!request) {
|
||||
this.setStatus("Selected request is no longer available.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetGroupID = request.groupId || "";
|
||||
if (!targetGroupID) {
|
||||
this.setStatus(
|
||||
"Selected request has no owning group to target.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetGroup = this.groups.find(
|
||||
(group) => (group.groupId || "") === targetGroupID,
|
||||
);
|
||||
if (!targetGroup) {
|
||||
this.setStatus(
|
||||
"Selected request group is no longer available.",
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.convertingRequestId = requestID;
|
||||
this.populateOrderModal({
|
||||
selectedAssigneeID:
|
||||
this.getSortedGroups().find(
|
||||
(group) => (group.groupId || "") !== targetGroupID,
|
||||
)?.groupId || "",
|
||||
selectedTargetID: targetGroupID,
|
||||
note: this.buildRequestOrderNote(request),
|
||||
priority: request.priority || "priority",
|
||||
});
|
||||
document.getElementById("dispatcherOrderModalTitle").textContent =
|
||||
"Create Order From Request";
|
||||
document
|
||||
.getElementById("dispatcherOrderModal")
|
||||
.classList.remove("is-hidden");
|
||||
this.setStatus("Preparing dispatch order from request...", "info");
|
||||
},
|
||||
convertViewedRequestToOrder() {
|
||||
if (!this.viewingRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestID = this.viewingRequestId;
|
||||
this.closeRequestModal();
|
||||
this.convertRequestToOrder(requestID);
|
||||
},
|
||||
populateOrderModal(options = {}) {
|
||||
const sortedGroups = this.getSortedGroups();
|
||||
const assigneeSelect = document.getElementById(
|
||||
"dispatcherOrderAssigneeSelect",
|
||||
);
|
||||
const targetSelect = document.getElementById(
|
||||
"dispatcherOrderTargetSelect",
|
||||
);
|
||||
const noteInput = document.getElementById("dispatcherOrderNoteInput");
|
||||
const prioritySelect = document.getElementById(
|
||||
"dispatcherOrderPrioritySelect",
|
||||
);
|
||||
if (!assigneeSelect || !targetSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAssigneeID = options.selectedAssigneeID || "";
|
||||
const selectedTargetID = options.selectedTargetID || "";
|
||||
const fallbackAssignee =
|
||||
selectedAssigneeID ||
|
||||
sortedGroups.find(
|
||||
(group) => (group.groupId || "") !== selectedTargetID,
|
||||
)?.groupId ||
|
||||
sortedGroups[0]?.groupId ||
|
||||
"";
|
||||
const fallbackTarget =
|
||||
selectedTargetID ||
|
||||
sortedGroups.find(
|
||||
(group) => (group.groupId || "") !== fallbackAssignee,
|
||||
)?.groupId ||
|
||||
sortedGroups[0]?.groupId ||
|
||||
"";
|
||||
|
||||
assigneeSelect.innerHTML = this.buildGroupOptions(fallbackAssignee);
|
||||
targetSelect.innerHTML = this.buildGroupOptions(fallbackTarget);
|
||||
if (noteInput) {
|
||||
noteInput.value = options.note || "";
|
||||
}
|
||||
if (prioritySelect) {
|
||||
prioritySelect.value = options.priority || "priority";
|
||||
}
|
||||
},
|
||||
syncOrderModal() {
|
||||
const modalEl = document.getElementById("dispatcherOrderModal");
|
||||
if (!modalEl || modalEl.classList.contains("is-hidden")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.populateOrderModal({
|
||||
selectedAssigneeID:
|
||||
document.getElementById("dispatcherOrderAssigneeSelect")
|
||||
?.value || "",
|
||||
selectedTargetID:
|
||||
document.getElementById("dispatcherOrderTargetSelect")?.value ||
|
||||
"",
|
||||
note:
|
||||
document.getElementById("dispatcherOrderNoteInput")?.value ||
|
||||
"",
|
||||
priority:
|
||||
document.getElementById("dispatcherOrderPrioritySelect")
|
||||
?.value || "priority",
|
||||
});
|
||||
},
|
||||
openGroupModal(groupID) {
|
||||
const group = this.groups.find((entry) => entry.groupId === groupID);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editingGroupId = groupID;
|
||||
document.getElementById("dispatcherModalGroupCallsign").textContent =
|
||||
group.callsign || group.groupId || "Unknown";
|
||||
document.getElementById("dispatcherModalGroupLeader").textContent =
|
||||
group.leaderName || "Unknown";
|
||||
document.getElementById("dispatcherModalGroupTask").textContent =
|
||||
group.currentTaskId || "None";
|
||||
document.getElementById("dispatcherModalGroupOrg").textContent =
|
||||
group.orgId || "default";
|
||||
document.getElementById("dispatcherModalRoleSelect").innerHTML =
|
||||
this.roles
|
||||
.map(
|
||||
(role) =>
|
||||
`<option value="${role}" ${role === group.role ? "selected" : ""}>${role.replaceAll("_", " ")}</option>`,
|
||||
)
|
||||
.join("");
|
||||
document.getElementById("dispatcherModalStatusSelect").innerHTML =
|
||||
this.statuses
|
||||
.map(
|
||||
(status) =>
|
||||
`<option value="${status}" ${status === group.status ? "selected" : ""}>${status.replaceAll("_", " ")}</option>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
document
|
||||
.getElementById("dispatcherGroupModal")
|
||||
.classList.remove("is-hidden");
|
||||
},
|
||||
closeGroupModal() {
|
||||
this.editingGroupId = "";
|
||||
document
|
||||
.getElementById("dispatcherGroupModal")
|
||||
.classList.add("is-hidden");
|
||||
},
|
||||
syncOpenModal() {
|
||||
if (!this.editingGroupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = this.groups.find(
|
||||
(entry) => entry.groupId === this.editingGroupId,
|
||||
);
|
||||
if (!group) {
|
||||
this.closeGroupModal();
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("dispatcherModalGroupCallsign").textContent =
|
||||
group.callsign || group.groupId || "Unknown";
|
||||
document.getElementById("dispatcherModalGroupLeader").textContent =
|
||||
group.leaderName || "Unknown";
|
||||
document.getElementById("dispatcherModalGroupTask").textContent =
|
||||
group.currentTaskId || "None";
|
||||
document.getElementById("dispatcherModalGroupOrg").textContent =
|
||||
group.orgId || "default";
|
||||
},
|
||||
};
|
||||
325
arma/client/addons/cad/ui/src/dispatcher/render.js
Normal file
325
arma/client/addons/cad/ui/src/dispatcher/render.js
Normal file
@ -0,0 +1,325 @@
|
||||
window.cadDispatcherRender = {
|
||||
updateDangerAlert() {
|
||||
const alertEl = document.getElementById("dispatcherDangerAlert");
|
||||
if (!alertEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dangerGroups = this.getDangerGroups();
|
||||
if (!dangerGroups.length) {
|
||||
alertEl.textContent = "";
|
||||
alertEl.classList.add("is-hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
const callsigns = dangerGroups.map(
|
||||
(group) => group.callsign || group.groupId || "Unknown Group",
|
||||
);
|
||||
alertEl.textContent = `Danger alert active: ${callsigns.join(", ")}`;
|
||||
alertEl.classList.remove("is-hidden");
|
||||
},
|
||||
updateRequestAlert() {
|
||||
const alertEl = document.getElementById("dispatcherRequestAlert");
|
||||
if (!alertEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alertMessage = this.buildSupportAlertMessage();
|
||||
if (!alertMessage) {
|
||||
alertEl.textContent = "";
|
||||
alertEl.classList.add("is-hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
alertEl.textContent = alertMessage;
|
||||
alertEl.classList.remove("is-hidden");
|
||||
},
|
||||
buildGroupEditorButton(groupID) {
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="dispatch-icon-btn"
|
||||
onclick="window.cadDispatcher.openGroupModal('${groupID}')"
|
||||
aria-label="Edit group"
|
||||
title="Edit group"
|
||||
>
|
||||
⚙
|
||||
</button>
|
||||
`;
|
||||
},
|
||||
buildCloseOrderButton(taskID) {
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="dispatch-btn dispatch-btn-secondary"
|
||||
onclick="window.cadDispatcher.closeDispatchOrder('${taskID}')"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
`;
|
||||
},
|
||||
buildCloseRequestButton(requestID) {
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="dispatch-btn dispatch-btn-secondary"
|
||||
onclick="event.stopPropagation(); window.cadDispatcher.closeSupportRequest('${requestID}')"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
`;
|
||||
},
|
||||
buildConvertRequestButton(requestID) {
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="dispatch-btn"
|
||||
onclick="event.stopPropagation(); window.cadDispatcher.convertRequestToOrder('${requestID}')"
|
||||
>
|
||||
Convert to Order
|
||||
</button>
|
||||
`;
|
||||
},
|
||||
renderMetrics() {
|
||||
const assignedContracts = this.contracts.filter(
|
||||
(entry) => (entry.assignmentState || "unassigned") !== "unassigned",
|
||||
);
|
||||
const openContracts = this.contracts.filter(
|
||||
(entry) => (entry.assignmentState || "unassigned") === "unassigned",
|
||||
);
|
||||
const openRequests = this.requests.length;
|
||||
const supportAlertRequests = this.getSupportAlertRequests();
|
||||
const dangerGroups = this.groups.filter(
|
||||
(group) => (group.status || "") === "danger",
|
||||
);
|
||||
|
||||
document.getElementById("metricOpenContracts").textContent =
|
||||
openContracts.length;
|
||||
document.getElementById("metricAssignedContracts").textContent =
|
||||
assignedContracts.length;
|
||||
document.getElementById("metricActiveGroups").textContent =
|
||||
this.groups.length;
|
||||
document.getElementById("metricOpenRequests").textContent =
|
||||
openRequests;
|
||||
document.getElementById("metricDangerGroups").textContent =
|
||||
dangerGroups.length;
|
||||
|
||||
const dangerMetricCard = document.getElementById(
|
||||
"metricDangerGroupsCard",
|
||||
);
|
||||
if (dangerMetricCard) {
|
||||
dangerMetricCard.classList.toggle(
|
||||
"is-danger",
|
||||
dangerGroups.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
const requestMetricCard = document.getElementById(
|
||||
"metricOpenRequestsCard",
|
||||
);
|
||||
if (requestMetricCard) {
|
||||
requestMetricCard.classList.toggle(
|
||||
"is-warning",
|
||||
supportAlertRequests.length > 0,
|
||||
);
|
||||
}
|
||||
},
|
||||
renderOpenContracts() {
|
||||
const container = document.getElementById("dispatcherOpenContracts");
|
||||
const openContracts = this.contracts.filter(
|
||||
(entry) => (entry.assignmentState || "unassigned") === "unassigned",
|
||||
);
|
||||
|
||||
if (!openContracts.length) {
|
||||
container.innerHTML =
|
||||
'<div class="placeholder-message"><p>No open contracts.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const groupOptions = this.buildGroupOptions("");
|
||||
|
||||
container.innerHTML = openContracts
|
||||
.map((task) => {
|
||||
const taskId = task.taskId || task.taskID || "";
|
||||
const position = Array.isArray(task.position)
|
||||
? task.position
|
||||
: [0, 0, 0];
|
||||
const targetGroup = this.groups.find(
|
||||
(group) => group.groupId === (task.targetGroupId || ""),
|
||||
);
|
||||
|
||||
return `
|
||||
<article class="dispatch-card">
|
||||
<header class="dispatch-card-header">
|
||||
<strong>${task.title || taskId}</strong>
|
||||
<span class="dispatch-badge">${this.formatTypeLabel(task)}</span>
|
||||
</header>
|
||||
<p class="dispatch-description">${task.description || ""}</p>
|
||||
<div class="dispatch-meta">
|
||||
<span>Unassigned</span>
|
||||
<span>${window.mapUI.formatPosition(position)}</span>
|
||||
</div>
|
||||
<div class="dispatch-meta">
|
||||
<span>Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"}</span>
|
||||
<span>Priority: ${(task.priority || "priority").replaceAll("_", " ")}</span>
|
||||
</div>
|
||||
<div class="dispatch-actions">
|
||||
<select id="dispatcher-assign-group-${taskId}" class="dispatch-select">
|
||||
<option value="">Assign to group</option>
|
||||
${groupOptions}
|
||||
</select>
|
||||
<button type="button" class="dispatch-btn" onclick="window.cadDispatcher.assignTask('${taskId}')">Assign</button>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
},
|
||||
renderAssignedContracts() {
|
||||
const container = document.getElementById(
|
||||
"dispatcherAssignedContracts",
|
||||
);
|
||||
const assignedContracts = this.contracts.filter(
|
||||
(entry) => (entry.assignmentState || "unassigned") !== "unassigned",
|
||||
);
|
||||
|
||||
if (!assignedContracts.length) {
|
||||
container.innerHTML =
|
||||
'<div class="placeholder-message"><p>No assigned contracts.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = assignedContracts
|
||||
.map((task) => {
|
||||
const taskId = task.taskId || task.taskID || "";
|
||||
const assignedGroup = this.groups.find(
|
||||
(group) => group.groupId === (task.assignedGroupId || ""),
|
||||
);
|
||||
const targetGroup = this.groups.find(
|
||||
(group) => group.groupId === (task.targetGroupId || ""),
|
||||
);
|
||||
const isDispatchOrder = this.isDispatchOrder(task);
|
||||
|
||||
return `
|
||||
<article class="dispatch-card">
|
||||
<header class="dispatch-card-header">
|
||||
<strong>${task.title || taskId}</strong>
|
||||
<span class="dispatch-badge">${task.assignmentState || "assigned"}</span>
|
||||
</header>
|
||||
<p class="dispatch-description">${task.description || ""}</p>
|
||||
<div class="dispatch-meta">
|
||||
<span>Group: ${assignedGroup ? assignedGroup.callsign : task.assignedGroupId || "Unknown"}</span>
|
||||
<span>Type: ${this.formatTypeLabel(task)}</span>
|
||||
</div>
|
||||
<div class="dispatch-meta">
|
||||
<span>Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"}</span>
|
||||
<span>Priority: ${(task.priority || "priority").replaceAll("_", " ")}</span>
|
||||
</div>
|
||||
${isDispatchOrder ? `<div class="dispatch-actions dispatch-actions-split">${this.buildCloseOrderButton(taskId)}</div>` : ""}
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
},
|
||||
renderGroups() {
|
||||
const container = document.getElementById("dispatcherGroups");
|
||||
if (!this.groups.length) {
|
||||
container.innerHTML =
|
||||
'<div class="placeholder-message"><p>No active groups available.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = this.getSortedGroups()
|
||||
.map((group) => {
|
||||
const isDanger = (group.status || "") === "danger";
|
||||
return `
|
||||
<article class="dispatch-card dispatch-card-group ${isDanger ? "is-danger" : ""}">
|
||||
<header class="dispatch-card-header">
|
||||
<div class="dispatch-card-header-main">
|
||||
<strong>${group.callsign || group.groupId}</strong>
|
||||
<span class="dispatch-badge">${group.role || "group"}</span>
|
||||
${isDanger ? '<span class="dispatch-alert-badge">Danger</span>' : ""}
|
||||
</div>
|
||||
<div class="dispatch-card-header-actions">
|
||||
${this.buildGroupEditorButton(group.groupId)}
|
||||
</div>
|
||||
</header>
|
||||
<div class="dispatch-meta">
|
||||
<span>Leader: ${group.leaderName || "Unknown"}</span>
|
||||
<span>Status: ${group.status || "unknown"}</span>
|
||||
</div>
|
||||
<div class="dispatch-meta">
|
||||
<span>Org: ${group.orgId || "default"}</span>
|
||||
<span>Task: ${group.currentTaskId || "None"}</span>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
},
|
||||
renderActivity() {
|
||||
const container = document.getElementById("dispatcherActivity");
|
||||
const requestsHTML = this.requests.length
|
||||
? this.requests
|
||||
.map(
|
||||
(request) => `
|
||||
<article class="dispatch-card dispatch-card-interactive ${["medevac_9line", "fire_support", "air_support"].includes(request.type || "") ? "is-warning" : ""}" onclick="window.cadDispatcher.openRequestModal('${request.requestId || ""}')">
|
||||
<header class="dispatch-card-header">
|
||||
<strong>${request.title || request.requestId || "Support Request"}</strong>
|
||||
<span class="dispatch-badge">${(request.priority || "priority").replaceAll("_", " ")}</span>
|
||||
</header>
|
||||
<p class="dispatch-description">${request.summary || ""}</p>
|
||||
<div class="dispatch-meta">
|
||||
<span>Group: ${request.groupCallsign || request.groupId || "Unknown"}</span>
|
||||
<span>${this.getRequestTypeLabel(request.type || "request")}</span>
|
||||
</div>
|
||||
<div class="dispatch-actions dispatch-actions-split">
|
||||
${this.buildConvertRequestButton(request.requestId || "")}
|
||||
${this.buildCloseRequestButton(request.requestId || "")}
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
: '<div class="placeholder-message"><p>No active support requests.</p></div>';
|
||||
|
||||
const activityHTML = this.activity.length
|
||||
? this.activity
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
(entry) => `
|
||||
<article class="dispatch-card">
|
||||
<header class="dispatch-card-header">
|
||||
<strong>${entry.type || "activity"}</strong>
|
||||
<span class="dispatch-badge">${Math.round(entry.timestamp || 0)}s</span>
|
||||
</header>
|
||||
<p class="dispatch-description">${entry.message || ""}</p>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join("")
|
||||
: '<div class="placeholder-message"><p>No recent activity.</p></div>';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="dispatch-inline-section">
|
||||
<div class="dispatch-inline-header">Support Requests</div>
|
||||
${requestsHTML}
|
||||
</div>
|
||||
<div class="dispatch-inline-section">
|
||||
<div class="dispatch-inline-header">Recent Activity</div>
|
||||
${activityHTML}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
render() {
|
||||
this.updateDangerAlert();
|
||||
this.updateRequestAlert();
|
||||
this.renderMetrics();
|
||||
this.renderOpenContracts();
|
||||
this.renderAssignedContracts();
|
||||
this.renderGroups();
|
||||
this.renderActivity();
|
||||
},
|
||||
};
|
||||
74
arma/client/addons/cad/ui/src/shared.js
Normal file
74
arma/client/addons/cad/ui/src/shared.js
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Shared JavaScript for Map UI
|
||||
* Provides common utilities and state management across all UI components
|
||||
*/
|
||||
|
||||
window.mapUIState = {
|
||||
layersPanelVisible: true,
|
||||
sidePanelElement: null,
|
||||
};
|
||||
|
||||
window.mapUI = {
|
||||
formatGridCoordinate(value) {
|
||||
return Math.round(Number(value) || 0)
|
||||
.toString()
|
||||
.padStart(4, "0");
|
||||
},
|
||||
formatPosition(position) {
|
||||
const safePosition = Array.isArray(position) ? position : [0, 0, 0];
|
||||
return `X: ${this.formatGridCoordinate(safePosition[0])} Y: ${this.formatGridCoordinate(safePosition[1])}`;
|
||||
},
|
||||
sendEvent(event, data) {
|
||||
A3API.SendAlert(JSON.stringify({ event: event, data: data }));
|
||||
},
|
||||
updateCoordinates(x, y) {
|
||||
const coordDisplay = document.getElementById("coordsDisplay");
|
||||
if (coordDisplay) {
|
||||
coordDisplay.textContent = this.formatPosition([x, y, 0]);
|
||||
}
|
||||
},
|
||||
updateScale(scale) {
|
||||
const scaleDisplay = document.getElementById("scaleDisplay");
|
||||
if (scaleDisplay) {
|
||||
scaleDisplay.textContent = `Scale: 1:${Math.round(scale)}`;
|
||||
}
|
||||
},
|
||||
updateStatus(text) {
|
||||
const statusText = document.getElementById("statusText");
|
||||
if (statusText) {
|
||||
statusText.textContent = text;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.updateCoordinates = window.mapUI.updateCoordinates;
|
||||
window.updateScale = window.mapUI.updateScale;
|
||||
window.updateStatus = window.mapUI.updateStatus;
|
||||
|
||||
window.ForgeBridge = window.ForgeBridge || {
|
||||
_handlers: {},
|
||||
on(event, handler) {
|
||||
this._handlers[event] = this._handlers[event] || [];
|
||||
this._handlers[event].push(handler);
|
||||
},
|
||||
ready(payload) {
|
||||
window.mapUI.sendEvent("cad::ready", payload || {});
|
||||
return true;
|
||||
},
|
||||
receive(payload) {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlers = this._handlers[payload.event] || [];
|
||||
handlers.forEach((handler) => handler(payload.data || {}));
|
||||
},
|
||||
send(event, data) {
|
||||
window.mapUI.sendEvent(event, data || {});
|
||||
return true;
|
||||
},
|
||||
close(data) {
|
||||
window.mapUI.sendEvent("map::close", data || {});
|
||||
return true;
|
||||
},
|
||||
};
|
||||
190
arma/client/addons/cad/ui/src/sidepanel.html
Normal file
190
arma/client/addons/cad/ui/src/sidepanel.html
Normal file
@ -0,0 +1,190 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="panel-header">
|
||||
<h3>CAD System</h3>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div id="cadStatusMessage" class="task-status-message"></div>
|
||||
<div id="cadDangerAlert" class="cad-danger-alert is-hidden"></div>
|
||||
<div id="cadRequestAlert" class="cad-warning-alert is-hidden"></div>
|
||||
<div class="cad-tabs" role="tablist" aria-label="CAD Sections">
|
||||
<button
|
||||
id="tabContractsBtn"
|
||||
class="cad-tab is-active"
|
||||
type="button"
|
||||
data-tab="contracts"
|
||||
>
|
||||
Contracts
|
||||
</button>
|
||||
<button
|
||||
id="tabRosterBtn"
|
||||
class="cad-tab"
|
||||
type="button"
|
||||
data-tab="roster"
|
||||
>
|
||||
Roster
|
||||
</button>
|
||||
<button
|
||||
id="tabRequestsBtn"
|
||||
class="cad-tab"
|
||||
type="button"
|
||||
data-tab="requests"
|
||||
>
|
||||
Requests
|
||||
</button>
|
||||
<button
|
||||
id="tabActivityBtn"
|
||||
class="cad-tab"
|
||||
type="button"
|
||||
data-tab="activity"
|
||||
>
|
||||
Activity
|
||||
</button>
|
||||
</div>
|
||||
<div class="cad-tab-panels">
|
||||
<div
|
||||
id="contractsPanel"
|
||||
class="cad-section is-active"
|
||||
data-panel="contracts"
|
||||
>
|
||||
<div class="cad-section-header">Contracts</div>
|
||||
<div id="taskList" class="task-list">
|
||||
<div class="placeholder-message">
|
||||
<p>Loading contracts...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rosterPanel" class="cad-section" data-panel="roster">
|
||||
<div class="cad-section-header">Roster</div>
|
||||
<div id="rosterList" class="task-list">
|
||||
<div class="placeholder-message">
|
||||
<p>Loading roster...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="requestsPanel"
|
||||
class="cad-section"
|
||||
data-panel="requests"
|
||||
>
|
||||
<div class="cad-section-header">Support Requests</div>
|
||||
<div id="requestList" class="task-list">
|
||||
<div class="placeholder-message">
|
||||
<p>No support requests.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="activityPanel"
|
||||
class="cad-section"
|
||||
data-panel="activity"
|
||||
>
|
||||
<div class="cad-section-header">Activity</div>
|
||||
<div id="activityList" class="task-list">
|
||||
<div class="placeholder-message">
|
||||
<p>No recent activity.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cadRequestModal" class="cad-modal is-hidden">
|
||||
<div class="cad-modal-backdrop"></div>
|
||||
<div
|
||||
class="cad-modal-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cadRequestModalTitle"
|
||||
>
|
||||
<div class="cad-modal-header">
|
||||
<div>
|
||||
<div class="cad-section-header">Support Request</div>
|
||||
<h3 id="cadRequestModalTitle">Submit Request</h3>
|
||||
</div>
|
||||
<button
|
||||
id="cadRequestModalCloseBtn"
|
||||
class="cad-icon-btn"
|
||||
type="button"
|
||||
aria-label="Close support request form"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<div class="cad-modal-body">
|
||||
<div class="cad-modal-fields">
|
||||
<label class="cad-field">
|
||||
<span>Priority</span>
|
||||
<select
|
||||
id="cadRequestPrioritySelect"
|
||||
class="cad-select"
|
||||
>
|
||||
<option value="routine">routine</option>
|
||||
<option value="priority" selected>
|
||||
priority
|
||||
</option>
|
||||
<option value="emergency">emergency</option>
|
||||
</select>
|
||||
</label>
|
||||
<div
|
||||
id="cadRequestFields"
|
||||
class="cad-modal-fields"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cad-modal-actions">
|
||||
<button
|
||||
id="cadRequestModalSaveBtn"
|
||||
type="button"
|
||||
class="task-accept-btn"
|
||||
>
|
||||
Submit Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.MapLoader = {
|
||||
loadCSS(path) {
|
||||
return A3API.RequestFile(path).then((css) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
},
|
||||
loadJS(path) {
|
||||
return A3API.RequestFile(path).then((js) => {
|
||||
eval(js);
|
||||
});
|
||||
},
|
||||
loadAll(resources) {
|
||||
return resources.reduce((promise, resource) => {
|
||||
return promise.then(() => {
|
||||
if (resource.endsWith(".css")) {
|
||||
return this.loadCSS(resource);
|
||||
}
|
||||
|
||||
if (resource.endsWith(".js")) {
|
||||
return this.loadJS(resource);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}, Promise.resolve());
|
||||
},
|
||||
};
|
||||
|
||||
MapLoader.loadAll([
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-sidepanel.js",
|
||||
]).catch((err) => console.error("[SIDEPANEL] Load error:", err));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1238
arma/client/addons/cad/ui/src/sidepanel.js
Normal file
1238
arma/client/addons/cad/ui/src/sidepanel.js
Normal file
File diff suppressed because it is too large
Load Diff
40
arma/client/addons/cad/ui/src/styles/bottombar.css
Normal file
40
arma/client/addons/cad/ui/src/styles/bottombar.css
Normal file
@ -0,0 +1,40 @@
|
||||
body {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(14, 19, 27, 0.96),
|
||||
rgba(18, 23, 32, 0.93) 55%,
|
||||
rgba(13, 18, 25, 0.96)
|
||||
);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.14);
|
||||
box-shadow: 0 -12px 26px rgba(0, 0, 0, 0.24);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer-brand,
|
||||
.footer-version {
|
||||
color: rgba(245, 248, 255, 0.8);
|
||||
font-size: 12px;
|
||||
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
color: rgba(245, 248, 255, 0.62);
|
||||
}
|
||||
78
arma/client/addons/cad/ui/src/styles/common.css
Normal file
78
arma/client/addons/cad/ui/src/styles/common.css
Normal file
@ -0,0 +1,78 @@
|
||||
:root {
|
||||
--bg: rgba(9, 12, 18, 0.82);
|
||||
--panel: rgba(20, 24, 33, 0.9);
|
||||
--panel2: rgba(17, 21, 30, 0.82);
|
||||
--stroke: rgba(255, 255, 255, 0.12);
|
||||
--stroke2: rgba(255, 255, 255, 0.2);
|
||||
--text: rgba(245, 248, 255, 0.92);
|
||||
--muted: rgba(245, 248, 255, 0.62);
|
||||
--muted2: rgba(245, 248, 255, 0.42);
|
||||
--accent: rgba(104, 196, 255, 0.95);
|
||||
--danger: rgba(255, 96, 96, 0.95);
|
||||
--shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
|
||||
--radius: 14px;
|
||||
--radius2: 10px;
|
||||
--font:
|
||||
ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius2);
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.16s ease,
|
||||
border-color 0.16s ease,
|
||||
transform 0.16s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: rgba(255, 96, 96, 0.1);
|
||||
border-color: rgba(255, 96, 96, 0.25);
|
||||
color: rgba(255, 220, 220, 0.95);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
background: rgba(255, 96, 96, 0.2);
|
||||
border-color: rgba(255, 96, 96, 0.35);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 999px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
562
arma/client/addons/cad/ui/src/styles/dispatcher.css
Normal file
562
arma/client/addons/cad/ui/src/styles/dispatcher.css
Normal file
@ -0,0 +1,562 @@
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at top left,
|
||||
rgba(41, 69, 93, 0.18),
|
||||
transparent 30%
|
||||
),
|
||||
linear-gradient(180deg, rgba(9, 14, 20, 0.96), rgba(15, 22, 31, 0.98));
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.dispatch-shell {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 18px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dispatch-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dispatch-kicker {
|
||||
margin: 0 0 4px;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dispatch-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.dispatch-header button,
|
||||
.dispatch-btn,
|
||||
.dispatch-select {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(24, 31, 40, 0.9);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dispatch-header button,
|
||||
.dispatch-btn {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dispatch-btn-secondary {
|
||||
background: rgba(53, 40, 39, 0.92);
|
||||
}
|
||||
|
||||
.dispatch-status {
|
||||
min-height: 20px;
|
||||
font-size: 13px;
|
||||
color: rgba(233, 241, 248, 0.78);
|
||||
}
|
||||
|
||||
.dispatch-status[data-type="success"] {
|
||||
color: #79d28a;
|
||||
}
|
||||
|
||||
.dispatch-status[data-type="error"] {
|
||||
color: #ff8a80;
|
||||
}
|
||||
|
||||
.dispatch-danger-alert {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.38);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(92, 18, 18, 0.94),
|
||||
rgba(128, 29, 29, 0.82)
|
||||
);
|
||||
color: #ffd4cf;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
animation: cad-danger-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dispatch-danger-alert.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dispatch-warning-alert {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(246, 198, 84, 0.42);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(89, 64, 12, 0.94),
|
||||
rgba(125, 92, 18, 0.84)
|
||||
);
|
||||
color: #ffe9b2;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
animation: cad-warning-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dispatch-warning-alert.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dispatch-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(13, 19, 26, 0.72);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(233, 241, 248, 0.6);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.metric-card strong {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-card.is-danger {
|
||||
border-color: rgba(255, 107, 107, 0.34);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(74, 17, 17, 0.86),
|
||||
rgba(22, 13, 16, 0.92)
|
||||
);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 107, 107, 0.12);
|
||||
animation: cad-danger-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.metric-card.is-warning {
|
||||
border-color: rgba(246, 198, 84, 0.34);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(92, 65, 14, 0.86),
|
||||
rgba(29, 22, 11, 0.92)
|
||||
);
|
||||
box-shadow: inset 0 0 0 1px rgba(246, 198, 84, 0.12);
|
||||
animation: cad-warning-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dispatch-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.dispatch-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(11, 17, 24, 0.78);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dispatch-panel-open {
|
||||
grid-column: span 5;
|
||||
}
|
||||
|
||||
.dispatch-panel-assigned {
|
||||
grid-column: span 7;
|
||||
}
|
||||
|
||||
.dispatch-panel-groups {
|
||||
grid-column: span 8;
|
||||
}
|
||||
|
||||
.dispatch-panel-activity {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.dispatch-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dispatch-panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.dispatch-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.dispatch-inline-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dispatch-inline-header {
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.dispatch-card {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(19, 26, 34, 0.72);
|
||||
}
|
||||
|
||||
.dispatch-card-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dispatch-card-interactive:hover {
|
||||
border-color: rgba(91, 187, 255, 0.2);
|
||||
background: rgba(23, 31, 40, 0.82);
|
||||
}
|
||||
|
||||
.dispatch-card-header,
|
||||
.dispatch-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dispatch-card-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dispatch-card-header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dispatch-card-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dispatch-description {
|
||||
margin: 0 0 10px;
|
||||
line-height: 1.45;
|
||||
color: rgba(241, 246, 251, 0.82);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dispatch-meta {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: rgba(229, 237, 244, 0.7);
|
||||
}
|
||||
|
||||
.dispatch-badge {
|
||||
padding: 3px 7px;
|
||||
border: 1px solid rgba(91, 187, 255, 0.18);
|
||||
background: rgba(16, 43, 61, 0.7);
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dispatch-alert-badge {
|
||||
padding: 3px 7px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.44);
|
||||
background: rgba(95, 23, 23, 0.88);
|
||||
color: #ffd8d1;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.dispatch-icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: rgba(24, 31, 40, 0.92);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dispatch-icon-btn:hover {
|
||||
background: rgba(32, 42, 52, 0.96);
|
||||
}
|
||||
|
||||
.dispatch-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dispatch-card.is-danger {
|
||||
border-color: rgba(255, 107, 107, 0.34);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(69, 20, 22, 0.78),
|
||||
rgba(28, 17, 21, 0.92)
|
||||
);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 107, 107, 0.1);
|
||||
animation: cad-danger-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dispatch-card.is-danger .dispatch-meta,
|
||||
.dispatch-card.is-danger .dispatch-description {
|
||||
color: rgba(255, 232, 228, 0.82);
|
||||
}
|
||||
|
||||
.dispatch-card.is-warning {
|
||||
border-color: rgba(246, 198, 84, 0.34);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(86, 64, 17, 0.78),
|
||||
rgba(34, 27, 16, 0.92)
|
||||
);
|
||||
box-shadow: inset 0 0 0 1px rgba(246, 198, 84, 0.1);
|
||||
animation: cad-warning-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.dispatch-card.is-warning .dispatch-meta,
|
||||
.dispatch-card.is-warning .dispatch-description {
|
||||
color: rgba(255, 243, 214, 0.84);
|
||||
}
|
||||
|
||||
.dispatch-actions-split {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.dispatch-select {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.dispatch-textarea {
|
||||
width: 100%;
|
||||
min-height: 92px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(24, 31, 40, 0.92);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.placeholder-message {
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
color: rgba(233, 241, 248, 0.6);
|
||||
}
|
||||
|
||||
.dispatch-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dispatch-modal.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dispatch-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(4, 8, 12, 0.72);
|
||||
}
|
||||
|
||||
.dispatch-modal-dialog {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(560px, calc(100% - 48px));
|
||||
max-height: calc(100vh - 64px);
|
||||
margin: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(11, 17, 24, 0.98);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
.dispatch-modal-header,
|
||||
.dispatch-modal-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.dispatch-modal-header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dispatch-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.dispatch-modal-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dispatch-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.dispatch-meta-grid strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dispatch-modal-fields {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dispatch-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dispatch-field span {
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(233, 241, 248, 0.7);
|
||||
}
|
||||
|
||||
.dispatch-modal-actions {
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dispatch-detail-block,
|
||||
.dispatch-detail-list {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(19, 26, 34, 0.72);
|
||||
}
|
||||
|
||||
.dispatch-detail-block {
|
||||
padding: 12px;
|
||||
color: rgba(241, 246, 251, 0.82);
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.dispatch-detail-list {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dispatch-detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 180px) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(14, 20, 28, 0.92);
|
||||
}
|
||||
|
||||
.dispatch-detail-label {
|
||||
color: rgba(233, 241, 248, 0.64);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.dispatch-detail-value {
|
||||
color: rgba(241, 246, 251, 0.84);
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@keyframes cad-danger-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 107, 107, 0.08),
|
||||
0 0 0 rgba(255, 107, 107, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 141, 141, 0.22),
|
||||
0 0 18px rgba(255, 107, 107, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cad-warning-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(246, 198, 84, 0.08),
|
||||
0 0 0 rgba(246, 198, 84, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(251, 212, 118, 0.22),
|
||||
0 0 18px rgba(246, 198, 84, 0.16);
|
||||
}
|
||||
}
|
||||
554
arma/client/addons/cad/ui/src/styles/sidepanel.css
Normal file
554
arma/client/addons/cad/ui/src/styles/sidepanel.css
Normal file
@ -0,0 +1,554 @@
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--panel);
|
||||
border-left: 1px solid var(--stroke);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
body {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 14px;
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.05),
|
||||
transparent
|
||||
);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 14px;
|
||||
height: calc(100% - 56px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.placeholder-message {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.placeholder-message p {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cad-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 5px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cad-tabs.is-two-col {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.cad-tabs.is-three-col {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.cad-tab {
|
||||
min-width: 0;
|
||||
padding: 8px 7px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: rgba(20, 27, 33, 0.88);
|
||||
color: rgba(243, 246, 249, 0.78);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cad-tab:hover {
|
||||
background: rgba(31, 40, 47, 0.94);
|
||||
color: #f3f6f9;
|
||||
}
|
||||
|
||||
.cad-tab.is-active {
|
||||
border-color: rgba(91, 187, 255, 0.42);
|
||||
background: rgba(15, 40, 58, 0.96);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.cad-tab-panels {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cad-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cad-section.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cad-section-header {
|
||||
margin-bottom: 8px;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.task-accept-btn,
|
||||
.task-secondary-btn,
|
||||
.cad-select {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(30, 37, 43, 0.9);
|
||||
color: #f3f6f9;
|
||||
}
|
||||
|
||||
.task-accept-btn,
|
||||
.task-secondary-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-accept-btn:hover,
|
||||
.task-secondary-btn:hover {
|
||||
background: rgba(46, 57, 66, 0.95);
|
||||
}
|
||||
|
||||
.task-accept-btn:disabled,
|
||||
.task-secondary-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.task-status-message {
|
||||
min-height: 18px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #cdd6dd;
|
||||
}
|
||||
|
||||
.task-status-message[data-type="success"] {
|
||||
color: #79d28a;
|
||||
}
|
||||
|
||||
.task-status-message[data-type="error"] {
|
||||
color: #ff8a80;
|
||||
}
|
||||
|
||||
.cad-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.cad-modal.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cad-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(4, 8, 12, 0.76);
|
||||
}
|
||||
|
||||
.cad-modal-dialog {
|
||||
position: relative;
|
||||
width: min(480px, calc(100% - 28px));
|
||||
margin: 32px auto 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(11, 17, 24, 0.98);
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
.cad-modal-header,
|
||||
.cad-modal-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.cad-modal-header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.cad-modal-header h3 {
|
||||
margin: 4px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.cad-modal-body {
|
||||
padding: 14px;
|
||||
max-height: 62vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cad-modal-fields {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cad-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cad-field span {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(233, 241, 248, 0.7);
|
||||
}
|
||||
|
||||
.cad-input,
|
||||
.cad-textarea {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(30, 37, 43, 0.9);
|
||||
color: #f3f6f9;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.cad-textarea {
|
||||
min-height: 74px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.cad-icon-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: rgba(24, 31, 40, 0.92);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cad-modal-actions {
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.cad-danger-alert {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.36);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(92, 18, 18, 0.94),
|
||||
rgba(128, 29, 29, 0.82)
|
||||
);
|
||||
color: #ffd4cf;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
animation: cad-danger-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cad-danger-alert.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cad-warning-alert {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(246, 198, 84, 0.4);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(89, 64, 12, 0.94),
|
||||
rgba(125, 92, 18, 0.84)
|
||||
);
|
||||
color: #ffe9b2;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
animation: cad-warning-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.cad-warning-alert.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cad-request-actions {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cad-request-btn {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.task-action-stack,
|
||||
.task-action-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-action-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(12, 16, 20, 0.62);
|
||||
}
|
||||
|
||||
.task-card.is-danger,
|
||||
.roster-summary-card.is-danger {
|
||||
border-color: rgba(255, 107, 107, 0.34);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(69, 20, 22, 0.78),
|
||||
rgba(28, 17, 21, 0.92)
|
||||
);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 107, 107, 0.1);
|
||||
animation: cad-danger-pulse 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.task-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.task-type {
|
||||
opacity: 0.7;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.task-secondary-btn {
|
||||
background: rgba(60, 48, 45, 0.92);
|
||||
}
|
||||
|
||||
.roster-summary-card {
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(16, 23, 29, 0.82);
|
||||
}
|
||||
|
||||
.task-alert-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.44);
|
||||
background: rgba(95, 23, 23, 0.88);
|
||||
color: #ffd8d1;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.roster-member-card {
|
||||
background: rgba(12, 16, 20, 0.74);
|
||||
}
|
||||
|
||||
.dispatch-map-group-card {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 120ms ease,
|
||||
background 120ms ease,
|
||||
transform 120ms ease;
|
||||
}
|
||||
|
||||
.dispatch-map-group-card strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dispatch-map-group-card .task-type {
|
||||
color: var(--accent);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.dispatch-map-group-card .task-meta {
|
||||
color: var(--muted);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dispatch-map-group-card:hover {
|
||||
border-color: rgba(91, 187, 255, 0.26);
|
||||
background: rgba(18, 29, 38, 0.9);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.dispatch-map-group-card.is-selected {
|
||||
border-color: rgba(91, 187, 255, 0.52);
|
||||
background: rgba(15, 40, 58, 0.92);
|
||||
box-shadow: inset 0 0 0 1px rgba(91, 187, 255, 0.18);
|
||||
}
|
||||
|
||||
.dispatch-map-group-card.is-danger:not(.is-selected) {
|
||||
border-color: rgba(255, 107, 107, 0.34);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(69, 20, 22, 0.78),
|
||||
rgba(28, 17, 21, 0.92)
|
||||
);
|
||||
}
|
||||
|
||||
.dispatch-map-group-card.is-danger .task-meta,
|
||||
.roster-summary-card.is-danger .task-meta {
|
||||
color: rgba(255, 232, 228, 0.82);
|
||||
}
|
||||
|
||||
.dispatch-map-card {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 120ms ease,
|
||||
background 120ms ease,
|
||||
transform 120ms ease;
|
||||
}
|
||||
|
||||
.dispatch-map-card strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dispatch-map-card .task-type {
|
||||
color: var(--accent);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.dispatch-map-card .task-description {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.dispatch-map-card .task-meta {
|
||||
color: var(--muted);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dispatch-map-card:hover {
|
||||
border-color: rgba(91, 187, 255, 0.26);
|
||||
background: rgba(18, 29, 38, 0.9);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.dispatch-map-card.is-selected {
|
||||
border-color: rgba(91, 187, 255, 0.52);
|
||||
background: rgba(15, 40, 58, 0.92);
|
||||
box-shadow: inset 0 0 0 1px rgba(91, 187, 255, 0.18);
|
||||
}
|
||||
|
||||
.dispatch-map-card.is-warning:not(.is-selected) {
|
||||
border-color: rgba(246, 198, 84, 0.34);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(86, 64, 17, 0.78),
|
||||
rgba(34, 27, 16, 0.92)
|
||||
);
|
||||
}
|
||||
|
||||
.dispatch-map-card.is-warning .task-meta,
|
||||
.dispatch-map-card.is-warning .task-description {
|
||||
color: rgba(255, 243, 214, 0.84);
|
||||
}
|
||||
|
||||
.roster-leader-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(91, 187, 255, 0.28);
|
||||
background: rgba(15, 40, 58, 0.82);
|
||||
color: var(--accent);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@keyframes cad-danger-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 107, 107, 0.08),
|
||||
0 0 0 rgba(255, 107, 107, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 141, 141, 0.22),
|
||||
0 0 14px rgba(255, 107, 107, 0.14);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cad-warning-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(246, 198, 84, 0.08),
|
||||
0 0 0 rgba(246, 198, 84, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(251, 212, 118, 0.22),
|
||||
0 0 18px rgba(246, 198, 84, 0.16);
|
||||
}
|
||||
}
|
||||
296
arma/client/addons/cad/ui/src/styles/topbar.css
Normal file
296
arma/client/addons/cad/ui/src/styles/topbar.css
Normal file
@ -0,0 +1,296 @@
|
||||
body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
padding: 0 16px;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body[data-mode="operations"] {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
body[data-mode="dispatch"] {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
height: 60px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(16, 22, 31, 0.96),
|
||||
rgba(19, 26, 36, 0.94) 55%,
|
||||
rgba(15, 20, 28, 0.96)
|
||||
);
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: var(--accent);
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.header-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title-kicker {
|
||||
color: rgba(218, 227, 236, 0.56);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.title-main {
|
||||
color: rgba(245, 248, 255, 0.92);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.operator-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.operator-strip.is-hidden,
|
||||
.operator-controls.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.operator-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 88px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.operator-label {
|
||||
color: rgba(218, 227, 236, 0.5);
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.operator-info strong {
|
||||
color: rgba(245, 248, 255, 0.9);
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.operator-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.operator-select {
|
||||
min-width: 92px;
|
||||
max-width: 112px;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: rgba(14, 20, 28, 0.96);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-operator {
|
||||
min-width: 84px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.mode-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.mode-controls.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dispatch-view-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.dispatch-view-controls.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.mode-text {
|
||||
color: rgba(233, 241, 248, 0.72);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
position: relative;
|
||||
width: 54px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mode-switch input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mode-slider {
|
||||
position: relative;
|
||||
width: 54px;
|
||||
height: 28px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 999px;
|
||||
background: rgba(22, 29, 39, 0.92);
|
||||
box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.22);
|
||||
transition:
|
||||
border-color 0.16s ease,
|
||||
background 0.16s ease;
|
||||
}
|
||||
|
||||
.mode-slider::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(237, 244, 251, 0.98),
|
||||
rgba(189, 205, 221, 0.92)
|
||||
);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.26);
|
||||
transition:
|
||||
transform 0.16s ease,
|
||||
background 0.16s ease;
|
||||
}
|
||||
|
||||
.mode-switch input:checked + .mode-slider {
|
||||
border-color: rgba(91, 187, 255, 0.42);
|
||||
background: rgba(14, 37, 56, 0.95);
|
||||
}
|
||||
|
||||
.mode-switch input:checked + .mode-slider::after {
|
||||
transform: translateX(26px);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(131, 212, 255, 0.98),
|
||||
rgba(72, 170, 231, 0.94)
|
||||
);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
min-width: 42px;
|
||||
}
|
||||
|
||||
.btn-dispatch-view {
|
||||
min-width: 66px;
|
||||
padding: 6px 10px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
min-width: 34px;
|
||||
width: 34px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
min-width: 40px;
|
||||
width: 40px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-dispatch-view.is-active {
|
||||
border-color: rgba(91, 187, 255, 0.42);
|
||||
background: rgba(15, 40, 58, 0.96);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body .logo,
|
||||
body .title-block,
|
||||
body .operator-strip,
|
||||
body .operator-controls,
|
||||
body .mode-controls,
|
||||
body .dispatch-view-controls,
|
||||
body .controls,
|
||||
body .mode-switch,
|
||||
body .mode-switch *,
|
||||
body button,
|
||||
body select,
|
||||
body label {
|
||||
pointer-events: auto;
|
||||
}
|
||||
132
arma/client/addons/cad/ui/src/topbar.html
Normal file
132
arma/client/addons/cad/ui/src/topbar.html
Normal file
@ -0,0 +1,132 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="logo">FORGE OS</div>
|
||||
<div class="header-main">
|
||||
<div class="title-block">
|
||||
<span class="title-kicker">Cad Systems</span>
|
||||
<strong class="title-main">FORGE Command & Dispatch</strong>
|
||||
</div>
|
||||
<div id="operatorStrip" class="operator-strip is-hidden">
|
||||
<div class="operator-info">
|
||||
<span class="operator-label">Current Group</span>
|
||||
<strong id="operatorGroupName">No Group</strong>
|
||||
</div>
|
||||
<div class="operator-info">
|
||||
<span class="operator-label">Location</span>
|
||||
<strong id="operatorLocation">Unavailable</strong>
|
||||
</div>
|
||||
<div id="operatorControls" class="operator-controls is-hidden">
|
||||
<select id="operatorRoleSelect" class="operator-select">
|
||||
<option value="infantry">infantry</option>
|
||||
<option value="recon">recon</option>
|
||||
<option value="armor">armor</option>
|
||||
<option value="air">air</option>
|
||||
<option value="logistics">logistics</option>
|
||||
<option value="support">support</option>
|
||||
</select>
|
||||
<button
|
||||
id="operatorRoleBtn"
|
||||
class="btn btn-operator"
|
||||
type="button"
|
||||
>
|
||||
Update Role
|
||||
</button>
|
||||
<select id="operatorStatusSelect" class="operator-select">
|
||||
<option value="available">available</option>
|
||||
<option value="en_route">en route</option>
|
||||
<option value="on_task">on task</option>
|
||||
<option value="holding">holding</option>
|
||||
<option value="danger">danger</option>
|
||||
<option value="unavailable">unavailable</option>
|
||||
</select>
|
||||
<button
|
||||
id="operatorStatusBtn"
|
||||
class="btn btn-operator"
|
||||
type="button"
|
||||
>
|
||||
Update Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modeControls" class="mode-controls is-hidden">
|
||||
<span class="mode-text">Ops</span>
|
||||
<label class="mode-switch" for="modeToggle">
|
||||
<input id="modeToggle" type="checkbox" />
|
||||
<span class="mode-slider"></span>
|
||||
</label>
|
||||
<span class="mode-text">Dispatch</span>
|
||||
</div>
|
||||
<div id="dispatchViewControls" class="dispatch-view-controls is-hidden">
|
||||
<button
|
||||
id="dispatchBoardBtn"
|
||||
class="btn btn-dispatch-view is-active"
|
||||
type="button"
|
||||
>
|
||||
Board
|
||||
</button>
|
||||
<button
|
||||
id="dispatchMapBtn"
|
||||
class="btn btn-dispatch-view"
|
||||
type="button"
|
||||
>
|
||||
Map
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button
|
||||
id="dispatchRefreshBtn"
|
||||
class="btn btn-icon btn-refresh"
|
||||
type="button"
|
||||
aria-label="Refresh board"
|
||||
title="Refresh board"
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
<button id="btnClose" class="btn btn-icon btn-close">X</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.MapLoader = {
|
||||
loadCSS(path) {
|
||||
return A3API.RequestFile(path).then((css) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
},
|
||||
loadJS(path) {
|
||||
return A3API.RequestFile(path).then((js) => {
|
||||
eval(js);
|
||||
});
|
||||
},
|
||||
loadAll(resources) {
|
||||
return resources.reduce((promise, resource) => {
|
||||
return promise.then(() => {
|
||||
if (resource.endsWith(".css")) {
|
||||
return this.loadCSS(resource);
|
||||
}
|
||||
|
||||
if (resource.endsWith(".js")) {
|
||||
return this.loadJS(resource);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}, Promise.resolve());
|
||||
},
|
||||
};
|
||||
|
||||
MapLoader.loadAll([
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-common.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.css",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-shared.js",
|
||||
"forge\\forge_client\\addons\\cad\\ui\\_site\\cad-topbar.js",
|
||||
]).catch((err) => console.error("[TOPBAR] Load error:", err));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
162
arma/client/addons/cad/ui/src/topbar.js
Normal file
162
arma/client/addons/cad/ui/src/topbar.js
Normal file
@ -0,0 +1,162 @@
|
||||
window.cadTopbar = {
|
||||
mode: "operations",
|
||||
dispatchView: "board",
|
||||
currentGroup: null,
|
||||
session: {},
|
||||
init() {
|
||||
document.getElementById("btnClose").addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("map::close", null);
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("modeToggle")
|
||||
.addEventListener("change", (event) => {
|
||||
window.mapUI.sendEvent("cad::mode::set", {
|
||||
mode: event.target.checked ? "dispatch" : "operations",
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatchRefreshBtn")
|
||||
.addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("cad::refresh", {});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatchBoardBtn")
|
||||
.addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("cad::dispatchView::set", {
|
||||
dispatchView: "board",
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("dispatchMapBtn")
|
||||
.addEventListener("click", () => {
|
||||
window.mapUI.sendEvent("cad::dispatchView::set", {
|
||||
dispatchView: "map",
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("operatorRoleBtn")
|
||||
.addEventListener("click", () => {
|
||||
if (!this.currentGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.mapUI.sendEvent("cad::groups::role", {
|
||||
groupID: this.currentGroup.groupId || "",
|
||||
role: document.getElementById("operatorRoleSelect").value,
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("operatorStatusBtn")
|
||||
.addEventListener("click", () => {
|
||||
if (!this.currentGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.mapUI.sendEvent("cad::groups::status", {
|
||||
groupID: this.currentGroup.groupId || "",
|
||||
status: document.getElementById("operatorStatusSelect")
|
||||
.value,
|
||||
});
|
||||
});
|
||||
|
||||
window.mapUI.sendEvent("cad::topbar::ready", {});
|
||||
},
|
||||
formatLocation(group) {
|
||||
const position = Array.isArray(group?.position)
|
||||
? group.position
|
||||
: [0, 0, 0];
|
||||
return window.mapUI.formatPosition(position);
|
||||
},
|
||||
receiveState(payload) {
|
||||
this.session =
|
||||
payload && payload.session && typeof payload.session === "object"
|
||||
? payload.session
|
||||
: {};
|
||||
this.mode =
|
||||
payload && typeof payload.mode === "string"
|
||||
? payload.mode
|
||||
: "operations";
|
||||
this.dispatchView =
|
||||
payload && typeof payload.dispatchView === "string"
|
||||
? payload.dispatchView
|
||||
: "board";
|
||||
this.currentGroup =
|
||||
payload &&
|
||||
payload.currentGroup &&
|
||||
typeof payload.currentGroup === "object"
|
||||
? payload.currentGroup
|
||||
: null;
|
||||
|
||||
const modeControls = document.getElementById("modeControls");
|
||||
const canDispatch = !!this.session.isDispatcher;
|
||||
const canOperateGroup =
|
||||
!!this.currentGroup &&
|
||||
(!!this.session.isLeader || !!this.session.isDispatcher);
|
||||
const operatorStrip = document.getElementById("operatorStrip");
|
||||
const operatorControls = document.getElementById("operatorControls");
|
||||
const dispatchViewControls = document.getElementById(
|
||||
"dispatchViewControls",
|
||||
);
|
||||
const dispatchRefreshBtn =
|
||||
document.getElementById("dispatchRefreshBtn");
|
||||
const dispatchBoardBtn = document.getElementById("dispatchBoardBtn");
|
||||
const dispatchMapBtn = document.getElementById("dispatchMapBtn");
|
||||
|
||||
modeControls.classList.toggle("is-hidden", !canDispatch);
|
||||
dispatchViewControls.classList.toggle(
|
||||
"is-hidden",
|
||||
!canDispatch || this.mode !== "dispatch",
|
||||
);
|
||||
operatorStrip.classList.toggle(
|
||||
"is-hidden",
|
||||
this.mode !== "operations" || !this.currentGroup,
|
||||
);
|
||||
operatorControls.classList.toggle("is-hidden", !canOperateGroup);
|
||||
|
||||
document.body.dataset.mode = this.mode;
|
||||
document.body.dataset.dispatcher = canDispatch ? "true" : "false";
|
||||
|
||||
document.getElementById("modeToggle").checked =
|
||||
this.mode === "dispatch";
|
||||
dispatchBoardBtn.classList.toggle(
|
||||
"is-active",
|
||||
this.dispatchView === "board",
|
||||
);
|
||||
dispatchMapBtn.classList.toggle(
|
||||
"is-active",
|
||||
this.dispatchView === "map",
|
||||
);
|
||||
dispatchRefreshBtn.title =
|
||||
this.mode === "dispatch" ? "Refresh dispatch board" : "Refresh CAD";
|
||||
dispatchRefreshBtn.setAttribute(
|
||||
"aria-label",
|
||||
this.mode === "dispatch" ? "Refresh dispatch board" : "Refresh CAD",
|
||||
);
|
||||
|
||||
document.getElementById("operatorGroupName").textContent = this
|
||||
.currentGroup
|
||||
? this.currentGroup.callsign ||
|
||||
this.currentGroup.groupId ||
|
||||
"Current Group"
|
||||
: "No Group";
|
||||
document.getElementById("operatorLocation").textContent = this
|
||||
.currentGroup
|
||||
? this.formatLocation(this.currentGroup)
|
||||
: "Unavailable";
|
||||
|
||||
if (this.currentGroup) {
|
||||
document.getElementById("operatorRoleSelect").value =
|
||||
this.currentGroup.role || "infantry";
|
||||
document.getElementById("operatorStatusSelect").value =
|
||||
this.currentGroup.status || "available";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.cadTopbar.init();
|
||||
89
arma/client/addons/cad/ui/ui.config.mjs
Normal file
89
arma/client/addons/cad/ui/ui.config.mjs
Normal file
@ -0,0 +1,89 @@
|
||||
export default {
|
||||
addonName: "cad",
|
||||
title: "FORGE CAD",
|
||||
logLabel: "CAD UI",
|
||||
outputDir: "_site",
|
||||
generateIndex: false,
|
||||
jsBundles: [
|
||||
{
|
||||
name: "CAD shared bridge/runtime",
|
||||
output: "cad-shared.js",
|
||||
sources: ["src/shared.js"],
|
||||
},
|
||||
{
|
||||
name: "CAD topbar app",
|
||||
output: "cad-topbar.js",
|
||||
sources: ["src/topbar.js"],
|
||||
},
|
||||
{
|
||||
name: "CAD sidepanel app",
|
||||
output: "cad-sidepanel.js",
|
||||
sources: ["src/sidepanel.js"],
|
||||
},
|
||||
{
|
||||
name: "CAD dispatcher app",
|
||||
output: "cad-dispatcher.js",
|
||||
sources: [
|
||||
"src/dispatcher/formatters.js",
|
||||
"src/dispatcher/modals.js",
|
||||
"src/dispatcher/render.js",
|
||||
"src/dispatcher/index.js",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "CAD bottombar app",
|
||||
output: "cad-bottombar.js",
|
||||
sources: ["src/bottombar.js"],
|
||||
},
|
||||
],
|
||||
cssBundles: [
|
||||
{
|
||||
name: "CAD common styles",
|
||||
output: "cad-common.css",
|
||||
sources: ["src/styles/common.css"],
|
||||
},
|
||||
{
|
||||
name: "CAD topbar styles",
|
||||
output: "cad-topbar.css",
|
||||
sources: ["src/styles/topbar.css"],
|
||||
},
|
||||
{
|
||||
name: "CAD sidepanel styles",
|
||||
output: "cad-sidepanel.css",
|
||||
sources: ["src/styles/sidepanel.css"],
|
||||
},
|
||||
{
|
||||
name: "CAD dispatcher styles",
|
||||
output: "cad-dispatcher.css",
|
||||
sources: ["src/styles/dispatcher.css"],
|
||||
},
|
||||
{
|
||||
name: "CAD bottombar styles",
|
||||
output: "cad-bottombar.css",
|
||||
sources: ["src/styles/bottombar.css"],
|
||||
},
|
||||
],
|
||||
htmlTemplates: [
|
||||
{
|
||||
name: "CAD topbar page",
|
||||
output: "topbar.html",
|
||||
source: "src/topbar.html",
|
||||
},
|
||||
{
|
||||
name: "CAD sidepanel page",
|
||||
output: "sidepanel.html",
|
||||
source: "src/sidepanel.html",
|
||||
},
|
||||
{
|
||||
name: "CAD dispatcher page",
|
||||
output: "dispatcher.html",
|
||||
source: "src/dispatcher.html",
|
||||
},
|
||||
{
|
||||
name: "CAD bottombar page",
|
||||
output: "bottombar.html",
|
||||
source: "src/bottombar.html",
|
||||
},
|
||||
],
|
||||
site: {},
|
||||
};
|
||||
@ -1,3 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initCatalogService);
|
||||
PREP(initClass);
|
||||
PREP(initSessionService);
|
||||
PREP(initActionService);
|
||||
PREP(initContextService);
|
||||
PREP(initHelperService);
|
||||
PREP(initPayloadService);
|
||||
PREP(initRepository);
|
||||
PREP(initUIBridge);
|
||||
PREP(initVGClass);
|
||||
PREP(initVGRepository);
|
||||
PREP(openUI);
|
||||
PREP(openVG);
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(GarageCatalogService)) then { call FUNC(initCatalogService); };
|
||||
if (isNil QGVAR(GarageClass)) then { call FUNC(initClass); };
|
||||
if (isNil QGVAR(GarageSessionService)) then { call FUNC(initSessionService); };
|
||||
if (isNil QGVAR(GarageHelperService)) then { call FUNC(initHelperService); };
|
||||
if (isNil QGVAR(GarageRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(GarageContextService)) then { call FUNC(initContextService); };
|
||||
if (isNil QGVAR(GaragePayloadService)) then { call FUNC(initPayloadService); };
|
||||
if (isNil QGVAR(GarageActionService)) then { call FUNC(initActionService); };
|
||||
if (isNil QGVAR(GarageUIBridge)) then { call FUNC(initUIBridge); };
|
||||
if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); };
|
||||
if (isNil QGVAR(VGRepository)) then { call FUNC(initVGRepository); };
|
||||
|
||||
[QGVAR(initGarage), {
|
||||
GVAR(GarageClass) call ["init", []];
|
||||
GVAR(GarageRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitGarage), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(GarageClass) call ["sync", [_data]];
|
||||
GVAR(GarageRepository) call ["sync", [_data]];
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||
};
|
||||
@ -22,7 +24,7 @@ if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); };
|
||||
[QGVAR(responseSyncGarage), {
|
||||
params [["_data", createHashMap, [createHashMap, []]]];
|
||||
|
||||
GVAR(GarageClass) call ["sync", [_data]];
|
||||
GVAR(GarageRepository) call ["sync", [_data]];
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||
};
|
||||
@ -31,35 +33,35 @@ if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); };
|
||||
[QGVAR(responseGarageAction), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleActionResponse", [_payload]];
|
||||
if !(isNil QGVAR(GarageActionService)) then {
|
||||
GVAR(GarageActionService) call ["handleActionResponse", [_payload]];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(initVG), {
|
||||
GVAR(VGClass) call ["init", []];
|
||||
GVAR(VGRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitVG), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(VGClass) call ["sync", [_data]];
|
||||
GVAR(VGRepository) call ["sync", [_data]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncVG), {
|
||||
params [["_data", createHashMap, [createHashMap, []]]];
|
||||
|
||||
GVAR(VGClass) call ["sync", [_data]];
|
||||
GVAR(VGRepository) call ["sync", [_data]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(bank,BankClass) get "isLoaded";
|
||||
EGVAR(bank,BankRepository) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initGarage), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
[{
|
||||
GVAR(GarageClass) get "isLoaded";
|
||||
GVAR(GarageRepository) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initVG), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
@ -44,13 +44,13 @@ switch (_event) do {
|
||||
};
|
||||
};
|
||||
case "garage::vehicle::retrieve::request": {
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleRetrieveRequest", [_data]];
|
||||
if !(isNil QGVAR(GarageActionService)) then {
|
||||
GVAR(GarageActionService) call ["handleRetrieveRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "garage::vehicle::store::request": {
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleStoreRequest", [_data]];
|
||||
if !(isNil QGVAR(GarageActionService)) then {
|
||||
GVAR(GarageActionService) call ["handleStoreRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "garage::refresh": {
|
||||
|
||||
133
arma/client/addons/garage/functions/fnc_initActionService.sqf
Normal file
133
arma/client/addons/garage/functions/fnc_initActionService.sqf
Normal file
@ -0,0 +1,133 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initActionService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the garage action service for retrieve and store world actions.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Garage action service object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initActionService;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageActionServiceBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["pendingStoreVehicle", objNull];
|
||||
_self set ["pendingRetrieve", createHashMap];
|
||||
}],
|
||||
["handleRetrieveRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _plate = _data getOrDefault ["plate", ""];
|
||||
if (_plate isEqualTo "") exitWith {
|
||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Select a stored vehicle to retrieve."]]]];
|
||||
};
|
||||
|
||||
private _garageMap = if (isNil QGVAR(GarageRepository)) then { createHashMap } else { GVAR(GarageRepository) call ["getState", []] };
|
||||
private _vehicleData = _garageMap getOrDefault [_plate, createHashMap];
|
||||
if (_vehicleData isEqualTo createHashMap) exitWith {
|
||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record could not be found."]]]];
|
||||
};
|
||||
|
||||
private _context = GVAR(GarageContextService) call ["getContext", []];
|
||||
private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
|
||||
private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player];
|
||||
private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
|
||||
private _blockingVehicles = [];
|
||||
{ _blockingVehicles pushBackUnique _x; } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]);
|
||||
{ _blockingVehicles pushBackUnique _x; } forEach (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius]);
|
||||
if (_blockingVehicles isNotEqualTo []) exitWith {
|
||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "The garage spawn area is blocked."]]]];
|
||||
};
|
||||
|
||||
private _className = _vehicleData getOrDefault ["classname", ""];
|
||||
if (_className isEqualTo "") exitWith {
|
||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record is missing a classname."]]]];
|
||||
};
|
||||
|
||||
private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"];
|
||||
_vehicle setDir _spawnHeading;
|
||||
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]);
|
||||
_vehicle setDamage (_vehicleData getOrDefault ["damage", 0]);
|
||||
|
||||
private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
|
||||
private _hitPointNames = _hitPoints getOrDefault ["names", []];
|
||||
private _hitPointValues = _hitPoints getOrDefault ["values", []];
|
||||
for "_index" from 0 to ((count _hitPointNames) - 1) do {
|
||||
_vehicle setHitPointDamage [_hitPointNames param [_index, ""], _hitPointValues param [_index, 0]];
|
||||
};
|
||||
|
||||
_vehicle setVariable ["forge_garage_plate", _plate, true];
|
||||
_vehicle setVariable ["forge_garage_owner_uid", getPlayerUID player, true];
|
||||
|
||||
_self set ["pendingRetrieve", createHashMapFromArray [["plate", _plate], ["vehicle", _vehicle]]];
|
||||
[SRPC(garage,requestRetrieveVehicle), [getPlayerUID player, _plate]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["handleStoreRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _netId = _data getOrDefault ["netId", ""];
|
||||
if (_netId isEqualTo "") exitWith {
|
||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::store::failure", createHashMapFromArray [["message", "Select a nearby vehicle to store."]]]];
|
||||
};
|
||||
|
||||
private _vehicle = objectFromNetId _netId;
|
||||
if (isNull _vehicle) exitWith {
|
||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::store::failure", createHashMapFromArray [["message", "The selected vehicle is no longer available."]]]];
|
||||
};
|
||||
|
||||
if (crew _vehicle isNotEqualTo []) exitWith {
|
||||
GVAR(GarageUIBridge) call ["sendEvent", ["garage::store::failure", createHashMapFromArray [["message", "All crew must exit the vehicle before storing it."]]]];
|
||||
};
|
||||
|
||||
private _rawHitPoints = getAllHitPointsDamage _vehicle;
|
||||
private _hitPointsJson = toJSON (createHashMapFromArray [["names", _rawHitPoints param [0, []]], ["selections", _rawHitPoints param [1, []]], ["values", _rawHitPoints param [2, []]]]);
|
||||
|
||||
_self set ["pendingStoreVehicle", _vehicle];
|
||||
[SRPC(garage,requestStoreVehicle), [getPlayerUID player, typeOf _vehicle, fuel _vehicle, damage _vehicle, _hitPointsJson]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["handleActionResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
private _action = _payload getOrDefault ["action", ""];
|
||||
private _success = _payload getOrDefault ["success", false];
|
||||
private _message = _payload getOrDefault ["message", "Garage action failed."];
|
||||
|
||||
switch (_action) do {
|
||||
case "retrieve": {
|
||||
private _pendingRetrieve = _self getOrDefault ["pendingRetrieve", createHashMap];
|
||||
private _vehicle = _pendingRetrieve getOrDefault ["vehicle", objNull];
|
||||
if (!_success && { !isNull _vehicle }) then { deleteVehicle _vehicle; };
|
||||
_self set ["pendingRetrieve", createHashMap];
|
||||
GVAR(GarageUIBridge) call ["sendEvent", [[ "garage::retrieve::failure", "garage::retrieve::success" ] select _success, createHashMapFromArray [["message", _message]]]];
|
||||
};
|
||||
case "store": {
|
||||
private _vehicle = _self getOrDefault ["pendingStoreVehicle", objNull];
|
||||
if (_success && { !isNull _vehicle }) then { deleteVehicle _vehicle; };
|
||||
_self set ["pendingStoreVehicle", objNull];
|
||||
GVAR(GarageUIBridge) call ["sendEvent", [[ "garage::store::failure", "garage::store::success" ] select _success, createHashMapFromArray [["message", _message]]]];
|
||||
};
|
||||
};
|
||||
|
||||
[] spawn {
|
||||
sleep 0.05;
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||
};
|
||||
};
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GarageActionService) = createHashMapObject [GVAR(GarageActionServiceBaseClass)];
|
||||
GVAR(GarageActionService)
|
||||
146
arma/client/addons/garage/functions/fnc_initContextService.sqf
Normal file
146
arma/client/addons/garage/functions/fnc_initContextService.sqf
Normal file
@ -0,0 +1,146 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initContextService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the garage context service for local garage context and nearby state.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Garage context service object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initContextService;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GarageContextServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageContextServiceBaseClass"],
|
||||
["#create", compileFinal { _self set ["lastContext", createHashMap]; }],
|
||||
["#delete", compileFinal { _self set ["lastContext", createHashMap]; }],
|
||||
["createDefaultContext", compileFinal {
|
||||
createHashMapFromArray [
|
||||
["name", "Vehicle Garage"],
|
||||
["anchorPosition", getPosATL player],
|
||||
["sourceObject", objNull],
|
||||
["spawnHeading", getDir player],
|
||||
["spawnPosition", player getPos [8, getDir player]],
|
||||
["spawnRadius", 6],
|
||||
["nearbyRadius", 30]
|
||||
]
|
||||
}],
|
||||
["scanEntryValues", compileFinal {
|
||||
params [["_values", [], [[]]], ["_state", createHashMap, [createHashMap]]];
|
||||
{
|
||||
if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then { _state set ["name", _x]; };
|
||||
if (_x isEqualType "") then {
|
||||
private _resolvedObject = _state getOrDefault ["sourceObject", objNull];
|
||||
if (isNull _resolvedObject) then {
|
||||
private _namedObject = missionNamespace getVariable [_x, objNull];
|
||||
if (!isNull _namedObject) then { _state set ["sourceObject", _namedObject]; };
|
||||
};
|
||||
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then { _state set ["anchorPosition", markerPos _x]; };
|
||||
continue;
|
||||
};
|
||||
if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then {
|
||||
_state set ["sourceObject", _x];
|
||||
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", getPosATL _x]; };
|
||||
continue;
|
||||
};
|
||||
if (_x isEqualType 0 && { (_state getOrDefault ["spawnHeading", -1]) < 0 }) then { _state set ["spawnHeading", _x]; continue; };
|
||||
if (_x isEqualType [] && { count _x > 0 }) then {
|
||||
if ({ _x isEqualType 0 } count _x >= 2 && { ((_state getOrDefault ["offset", []]) isEqualTo []) || ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) }) then {
|
||||
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", _x]; } else { _state set ["offset", _x]; };
|
||||
continue;
|
||||
};
|
||||
_self call ["scanEntryValues", [_x, _state]];
|
||||
};
|
||||
} forEach _values;
|
||||
_state
|
||||
}],
|
||||
["resolveEntry", compileFinal {
|
||||
params [["_entry", [], [[]]]];
|
||||
private _state = createHashMapFromArray [["name", "Vehicle Garage"], ["anchorPosition", []], ["sourceObject", objNull], ["offset", []], ["spawnHeading", -1]];
|
||||
_self call ["scanEntryValues", [_entry, _state]];
|
||||
private _anchorPosition = _state getOrDefault ["anchorPosition", []];
|
||||
private _offset = _state getOrDefault ["offset", []];
|
||||
private _spawnPosition = if (_anchorPosition isEqualTo []) then { [] } else { if (_offset isEqualTo []) then { _anchorPosition } else { _anchorPosition vectorAdd _offset } };
|
||||
createHashMapFromArray [["name", _state getOrDefault ["name", "Vehicle Garage"]], ["anchorPosition", _anchorPosition], ["sourceObject", _state getOrDefault ["sourceObject", objNull]], ["spawnHeading", _state getOrDefault ["spawnHeading", -1]], ["spawnPosition", _spawnPosition]]
|
||||
}],
|
||||
["resolveContext", compileFinal {
|
||||
private _context = _self call ["createDefaultContext", []];
|
||||
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
|
||||
if !(_locations isEqualType []) exitWith { _self set ["lastContext", _context]; _context };
|
||||
|
||||
private _nearestEntry = [];
|
||||
private _nearestDistance = 1e10;
|
||||
{
|
||||
private _entry = _self call ["resolveEntry", [_x]];
|
||||
private _anchorPosition = _entry getOrDefault ["anchorPosition", []];
|
||||
if (_anchorPosition isEqualTo []) then { continue; };
|
||||
private _distance = player distance2D _anchorPosition;
|
||||
if (_distance < _nearestDistance) then { _nearestDistance = _distance; _nearestEntry = _entry; };
|
||||
} forEach _locations;
|
||||
|
||||
if (_nearestEntry isEqualTo []) exitWith { _self set ["lastContext", _context]; _context };
|
||||
|
||||
private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []];
|
||||
private _garageObject = _nearestEntry getOrDefault ["sourceObject", objNull];
|
||||
private _garageName = _nearestEntry getOrDefault ["name", "Vehicle Garage"];
|
||||
private _spawnHeading = _nearestEntry getOrDefault ["spawnHeading", getDir player];
|
||||
if (_spawnHeading < 0) then { _spawnHeading = if (!isNull _garageObject) then { getDir _garageObject } else { getDir player }; };
|
||||
|
||||
private _spawnPosition = _nearestEntry getOrDefault ["spawnPosition", []];
|
||||
if (_spawnPosition isEqualTo []) then { _spawnPosition = if (_anchorPosition isEqualTo []) then { player getPos [8, _spawnHeading] } else { _anchorPosition }; };
|
||||
|
||||
_context set ["name", _garageName];
|
||||
_context set ["anchorPosition", _anchorPosition];
|
||||
_context set ["sourceObject", _garageObject];
|
||||
_context set ["spawnHeading", _spawnHeading];
|
||||
_context set ["spawnPosition", _spawnPosition];
|
||||
_self set ["lastContext", _context];
|
||||
_context
|
||||
}],
|
||||
["getContext", compileFinal { _self call ["resolveContext", []] }],
|
||||
["buildNearbyState", compileFinal {
|
||||
private _context = _self call ["getContext", []];
|
||||
private _anchorPosition = _context getOrDefault ["anchorPosition", []];
|
||||
private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
|
||||
private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
|
||||
private _nearbyRadius = _context getOrDefault ["nearbyRadius", 30];
|
||||
private _nearbyOrigin = [_anchorPosition, _spawnPosition] select (_anchorPosition isEqualTo []);
|
||||
private _nearbyVehicles = [];
|
||||
private _nearbyEntities = [];
|
||||
private _candidateVehicles = [];
|
||||
{ _candidateVehicles pushBackUnique _x; } forEach (_nearbyOrigin nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]);
|
||||
{ _candidateVehicles pushBackUnique _x; } forEach ((getPosATL player) nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]);
|
||||
{ _candidateVehicles pushBackUnique _x; } forEach (nearestObjects [_nearbyOrigin, ["AllVehicles"], _nearbyRadius]);
|
||||
{ _candidateVehicles pushBackUnique _x; } forEach (nearestObjects [getPosATL player, ["AllVehicles"], _nearbyRadius]);
|
||||
{
|
||||
if (isNull _x) then { continue; };
|
||||
if (_x isKindOf "CAManBase") then { continue; };
|
||||
if !(_x isKindOf "Car" || _x isKindOf "Tank" || _x isKindOf "Air" || _x isKindOf "Ship") then { continue; };
|
||||
_nearbyEntities pushBackUnique _x;
|
||||
} forEach _candidateVehicles;
|
||||
{
|
||||
if (isNull _x) then { continue; };
|
||||
private _builtVehicle = GVAR(GarageHelperService) call ["buildNearbyVehicle", [_x, _nearbyOrigin]];
|
||||
if (_builtVehicle isEqualTo createHashMap) then { continue; };
|
||||
_nearbyVehicles pushBack _builtVehicle;
|
||||
} forEach _nearbyEntities;
|
||||
private _nearbyVehiclePairs = _nearbyVehicles apply { [_x getOrDefault ["distance", 0], _x] };
|
||||
_nearbyVehiclePairs sort true;
|
||||
_nearbyVehicles = _nearbyVehiclePairs apply { _x param [1, createHashMap] };
|
||||
private _spawnBlocked = ((_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]) + (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius])) isNotEqualTo [];
|
||||
createHashMapFromArray [["session", createHashMapFromArray [["garageName", _context getOrDefault ["name", "Vehicle Garage"]], ["nearbyCount", count _nearbyVehicles], ["spawnBlocked", _spawnBlocked], ["spawnStatus", ["Ready", "Blocked"] select _spawnBlocked]]], ["nearby", createHashMapFromArray [["vehicles", _nearbyVehicles]]]]
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GarageContextService) = createHashMapObject [GVAR(GarageContextServiceBaseClass)];
|
||||
GVAR(GarageContextService)
|
||||
@ -1,18 +1,27 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initCatalogService.sqf
|
||||
* File: fnc_initHelperService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-14
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the garage catalog service for vehicle metadata and UI-friendly shaping.
|
||||
* Initializes the garage helper service for vehicle metadata and UI-friendly shaping.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Garage helper service object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initHelperService;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GarageCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageCatalogServiceBaseClass"],
|
||||
GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageHelperServiceBaseClass"],
|
||||
["resolveCategory", compileFinal {
|
||||
params [["_className", "", [""]]];
|
||||
|
||||
@ -156,5 +165,5 @@ GVAR(GarageCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GarageCatalogService) = createHashMapObject [GVAR(GarageCatalogServiceBaseClass)];
|
||||
GVAR(GarageCatalogService)
|
||||
GVAR(GarageHelperService) = createHashMapObject [GVAR(GarageHelperServiceBaseClass)];
|
||||
GVAR(GarageHelperService)
|
||||
@ -0,0 +1,44 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initPayloadService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the garage payload service for browser hydrate payload composition.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Garage payload service object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initPayloadService;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GaragePayloadServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GaragePayloadServiceBaseClass"],
|
||||
["buildStoredVehicles", compileFinal {
|
||||
private _garageMap = if (isNil QGVAR(GarageRepository)) then { createHashMap } else { GVAR(GarageRepository) call ["getState", []] };
|
||||
private _storedVehicles = [];
|
||||
{ _storedVehicles pushBack (GVAR(GarageHelperService) call ["buildStoredVehicle", [_x, _y]]); } forEach _garageMap;
|
||||
private _storedVehiclePairs = _storedVehicles apply { [toLowerANSI (_x getOrDefault ["displayName", ""]), _x] };
|
||||
_storedVehiclePairs sort true;
|
||||
_storedVehiclePairs apply { _x param [1, createHashMap] }
|
||||
}],
|
||||
["buildPayload", compileFinal {
|
||||
private _localState = GVAR(GarageContextService) call ["buildNearbyState", []];
|
||||
private _storedVehicles = _self call ["buildStoredVehicles", []];
|
||||
private _session = +(_localState getOrDefault ["session", createHashMap]);
|
||||
_session set ["capacityUsed", count _storedVehicles];
|
||||
_session set ["capacityMax", 5];
|
||||
createHashMapFromArray [["session", _session], ["garage", createHashMapFromArray [["vehicles", _storedVehicles]]], ["nearby", +(_localState getOrDefault ["nearby", createHashMap])]]
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GaragePayloadService) = createHashMapObject [GVAR(GaragePayloadServiceBaseClass)];
|
||||
GVAR(GaragePayloadService)
|
||||
@ -1,48 +1,44 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initClass.sqf
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-17
|
||||
* Last Update: 2026-02-13
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the Garage class for managing player vehicles.
|
||||
* Provides methods for syncing, saving, and applying vehicles to the player's garage.
|
||||
* Initializes the garage repository for persisted stored vehicle records.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Garage class object [HASHMAP OBJECT]
|
||||
* Garage repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initClass
|
||||
* call forge_client_garage_fnc_initRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageBaseClass"],
|
||||
GVAR(GarageRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["garage", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
private _garage = _self get "garage";
|
||||
[SRPC(garage,requestInitGarage), [_uid]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
[SRPC(garage,requestInitGarage), [_uid, _garage]] call CFUNC(serverEvent);
|
||||
|
||||
systemChat format ["Garage loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Garage] Garage Class Initialized!";
|
||||
systemChat format ["Garage loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:Garage] Garage Repository Initialized!";
|
||||
}],
|
||||
["save", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(garage,requestSaveGarage), [_uid]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", compileFinal {
|
||||
@ -50,14 +46,13 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
private _garage = createHashMap;
|
||||
|
||||
{ _garage set [_x, _y]; } forEach _data;
|
||||
_self set ["garage", _garage];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Garage] Sync completed";
|
||||
diag_log "[FORGE:Client:Garage] Repository sync completed";
|
||||
}],
|
||||
["getGarageState", compileFinal {
|
||||
["getState", compileFinal {
|
||||
_self getOrDefault ["garage", createHashMap]
|
||||
}],
|
||||
["get", compileFinal {
|
||||
@ -68,5 +63,5 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GarageClass) = createHashMapObject [GVAR(GarageBaseClass)];
|
||||
GVAR(GarageClass)
|
||||
GVAR(GarageRepository) = createHashMapObject [GVAR(GarageRepositoryBaseClass)];
|
||||
GVAR(GarageRepository)
|
||||
@ -1,298 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initSessionService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-14
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the typed garage session service responsible for resolving the
|
||||
* active garage context and building the browser hydrate payload.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
|
||||
GVAR(GarageSessionServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageSessionServiceBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["lastContext", createHashMap];
|
||||
}],
|
||||
["#delete", compileFinal {
|
||||
_self set ["lastContext", createHashMap];
|
||||
}],
|
||||
["createDefaultContext", compileFinal {
|
||||
createHashMapFromArray [
|
||||
["name", "Vehicle Garage"],
|
||||
["anchorPosition", getPosATL player],
|
||||
["sourceObject", objNull],
|
||||
["spawnHeading", getDir player],
|
||||
["spawnPosition", player getPos [8, getDir player]],
|
||||
["spawnRadius", 6],
|
||||
["nearbyRadius", 30]
|
||||
]
|
||||
}],
|
||||
["scanEntryValues", compileFinal {
|
||||
params [
|
||||
["_values", [], [[]]],
|
||||
["_state", createHashMap, [createHashMap]]
|
||||
];
|
||||
|
||||
{
|
||||
if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then {
|
||||
_state set ["name", _x];
|
||||
};
|
||||
|
||||
if (_x isEqualType "") then {
|
||||
private _resolvedObject = _state getOrDefault ["sourceObject", objNull];
|
||||
if (isNull _resolvedObject) then {
|
||||
private _namedObject = missionNamespace getVariable [_x, objNull];
|
||||
if (!isNull _namedObject) then {
|
||||
_state set ["sourceObject", _namedObject];
|
||||
};
|
||||
};
|
||||
|
||||
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then {
|
||||
_state set ["anchorPosition", markerPos _x];
|
||||
};
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then {
|
||||
_state set ["sourceObject", _x];
|
||||
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then {
|
||||
_state set ["anchorPosition", getPosATL _x];
|
||||
};
|
||||
continue;
|
||||
};
|
||||
|
||||
if (_x isEqualType 0 && { (_state getOrDefault ["spawnHeading", -1]) < 0 }) then {
|
||||
_state set ["spawnHeading", _x];
|
||||
continue;
|
||||
};
|
||||
|
||||
if (_x isEqualType [] && { count _x > 0 }) then {
|
||||
if (
|
||||
{ _x isEqualType 0 } count _x >= 2 &&
|
||||
{
|
||||
((_state getOrDefault ["offset", []]) isEqualTo []) ||
|
||||
((_state getOrDefault ["anchorPosition", []]) isEqualTo [])
|
||||
}
|
||||
) then {
|
||||
if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then {
|
||||
_state set ["anchorPosition", _x];
|
||||
} else {
|
||||
_state set ["offset", _x];
|
||||
};
|
||||
continue;
|
||||
};
|
||||
|
||||
_self call ["scanEntryValues", [_x, _state]];
|
||||
};
|
||||
} forEach _values;
|
||||
|
||||
_state
|
||||
}],
|
||||
["resolveEntry", compileFinal {
|
||||
params [["_entry", [], [[]]]];
|
||||
|
||||
private _state = createHashMapFromArray [
|
||||
["name", "Vehicle Garage"],
|
||||
["anchorPosition", []],
|
||||
["sourceObject", objNull],
|
||||
["offset", []],
|
||||
["spawnHeading", -1]
|
||||
];
|
||||
|
||||
_self call ["scanEntryValues", [_entry, _state]];
|
||||
|
||||
private _anchorPosition = _state getOrDefault ["anchorPosition", []];
|
||||
private _offset = _state getOrDefault ["offset", []];
|
||||
private _spawnPosition = if (_anchorPosition isEqualTo []) then {
|
||||
[]
|
||||
} else {
|
||||
if (_offset isEqualTo []) then {
|
||||
_anchorPosition
|
||||
} else {
|
||||
_anchorPosition vectorAdd _offset
|
||||
}
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["name", _state getOrDefault ["name", "Vehicle Garage"]],
|
||||
["anchorPosition", _anchorPosition],
|
||||
["sourceObject", _state getOrDefault ["sourceObject", objNull]],
|
||||
["spawnHeading", _state getOrDefault ["spawnHeading", -1]],
|
||||
["spawnPosition", _spawnPosition]
|
||||
]
|
||||
}],
|
||||
["resolveContext", compileFinal {
|
||||
private _context = _self call ["createDefaultContext", []];
|
||||
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
|
||||
if !(_locations isEqualType []) exitWith {
|
||||
_self set ["lastContext", _context];
|
||||
_context
|
||||
};
|
||||
|
||||
private _nearestEntry = [];
|
||||
private _nearestDistance = 1e10;
|
||||
|
||||
{
|
||||
private _entry = _self call ["resolveEntry", [_x]];
|
||||
private _anchorPosition = _entry getOrDefault ["anchorPosition", []];
|
||||
if (_anchorPosition isEqualTo []) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
private _distance = player distance2D _anchorPosition;
|
||||
if (_distance < _nearestDistance) then {
|
||||
_nearestDistance = _distance;
|
||||
_nearestEntry = _entry;
|
||||
};
|
||||
} forEach _locations;
|
||||
|
||||
if (_nearestEntry isEqualTo []) exitWith {
|
||||
_self set ["lastContext", _context];
|
||||
_context
|
||||
};
|
||||
|
||||
private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []];
|
||||
private _garageObject = _nearestEntry getOrDefault ["sourceObject", objNull];
|
||||
private _garageName = _nearestEntry getOrDefault ["name", "Vehicle Garage"];
|
||||
private _spawnHeading = _nearestEntry getOrDefault ["spawnHeading", getDir player];
|
||||
if (_spawnHeading < 0) then {
|
||||
_spawnHeading = if (!isNull _garageObject) then { getDir _garageObject } else { getDir player };
|
||||
};
|
||||
|
||||
private _spawnPosition = _nearestEntry getOrDefault ["spawnPosition", []];
|
||||
if (_spawnPosition isEqualTo []) then {
|
||||
_spawnPosition = if (_anchorPosition isEqualTo []) then {
|
||||
player getPos [8, _spawnHeading]
|
||||
} else {
|
||||
_anchorPosition
|
||||
};
|
||||
};
|
||||
|
||||
_context set ["name", _garageName];
|
||||
_context set ["anchorPosition", _anchorPosition];
|
||||
_context set ["sourceObject", _garageObject];
|
||||
_context set ["spawnHeading", _spawnHeading];
|
||||
_context set ["spawnPosition", _spawnPosition];
|
||||
|
||||
_self set ["lastContext", _context];
|
||||
_context
|
||||
}],
|
||||
["getContext", compileFinal {
|
||||
_self call ["resolveContext", []]
|
||||
}],
|
||||
["buildPayload", compileFinal {
|
||||
private _context = _self call ["getContext", []];
|
||||
private _garageMap = if (isNil QGVAR(GarageClass)) then {
|
||||
createHashMap
|
||||
} else {
|
||||
GVAR(GarageClass) call ["getGarageState", []]
|
||||
};
|
||||
|
||||
private _anchorPosition = _context getOrDefault ["anchorPosition", []];
|
||||
private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
|
||||
private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
|
||||
private _nearbyRadius = _context getOrDefault ["nearbyRadius", 30];
|
||||
private _nearbyOrigin = [_anchorPosition, _spawnPosition] select (_anchorPosition isEqualTo []);
|
||||
|
||||
private _storedVehicles = [];
|
||||
private _nearbyVehicles = [];
|
||||
private _nearbyEntities = [];
|
||||
private _candidateVehicles = [];
|
||||
|
||||
{
|
||||
_candidateVehicles pushBackUnique _x;
|
||||
} forEach (_nearbyOrigin nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]);
|
||||
{
|
||||
_candidateVehicles pushBackUnique _x;
|
||||
} forEach ((getPosATL player) nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]);
|
||||
{
|
||||
_candidateVehicles pushBackUnique _x;
|
||||
} forEach (nearestObjects [_nearbyOrigin, ["AllVehicles"], _nearbyRadius]);
|
||||
{
|
||||
_candidateVehicles pushBackUnique _x;
|
||||
} forEach (nearestObjects [getPosATL player, ["AllVehicles"], _nearbyRadius]);
|
||||
|
||||
{
|
||||
if (isNull _x) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
if (_x isKindOf "CAManBase") then {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !(
|
||||
_x isKindOf "Car" ||
|
||||
_x isKindOf "Tank" ||
|
||||
_x isKindOf "Air" ||
|
||||
_x isKindOf "Ship"
|
||||
) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
_nearbyEntities pushBackUnique _x;
|
||||
} forEach _candidateVehicles;
|
||||
|
||||
{
|
||||
_storedVehicles pushBack (
|
||||
GVAR(GarageCatalogService) call ["buildStoredVehicle", [_x, _y]]
|
||||
);
|
||||
} forEach _garageMap;
|
||||
|
||||
private _storedVehiclePairs = _storedVehicles apply {
|
||||
[toLowerANSI (_x getOrDefault ["displayName", ""]), _x]
|
||||
};
|
||||
_storedVehiclePairs sort true;
|
||||
_storedVehicles = _storedVehiclePairs apply { _x param [1, createHashMap] };
|
||||
|
||||
{
|
||||
if (isNull _x) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
private _builtVehicle = GVAR(GarageCatalogService) call ["buildNearbyVehicle", [_x, _nearbyOrigin]];
|
||||
if (_builtVehicle isEqualTo createHashMap) then {
|
||||
continue;
|
||||
};
|
||||
|
||||
_nearbyVehicles pushBack _builtVehicle;
|
||||
} forEach _nearbyEntities;
|
||||
|
||||
private _nearbyVehiclePairs = _nearbyVehicles apply {
|
||||
[_x getOrDefault ["distance", 0], _x]
|
||||
};
|
||||
_nearbyVehiclePairs sort true;
|
||||
_nearbyVehicles = _nearbyVehiclePairs apply { _x param [1, createHashMap] };
|
||||
|
||||
private _spawnBlocked = (
|
||||
(_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]) +
|
||||
(nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius])
|
||||
) isNotEqualTo [];
|
||||
|
||||
createHashMapFromArray [
|
||||
["session", createHashMapFromArray [
|
||||
["garageName", _context getOrDefault ["name", "Vehicle Garage"]],
|
||||
["capacityUsed", count _storedVehicles],
|
||||
["capacityMax", 5],
|
||||
["nearbyCount", count _nearbyVehicles],
|
||||
["spawnBlocked", _spawnBlocked],
|
||||
["spawnStatus", ["Ready", "Blocked"] select _spawnBlocked]
|
||||
]],
|
||||
["garage", createHashMapFromArray [
|
||||
["vehicles", _storedVehicles]
|
||||
]],
|
||||
["nearby", createHashMapFromArray [
|
||||
["vehicles", _nearbyVehicles]
|
||||
]]
|
||||
]
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GarageSessionService) = createHashMapObject [GVAR(GarageSessionServiceBaseClass)];
|
||||
GVAR(GarageSessionService)
|
||||
@ -3,11 +3,20 @@
|
||||
/*
|
||||
* File: fnc_initUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-14
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the garage UI bridge for browser control state and retrieve/store actions.
|
||||
* Initializes the garage UI bridge for browser control state and UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Garage UI bridge object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initUIBridge;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
@ -17,10 +26,6 @@ private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
|
||||
GVAR(GarageUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#base", _webUIBridgeDeclaration],
|
||||
["#type", "GarageUIBridgeBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["pendingStoreVehicle", objNull];
|
||||
_self set ["pendingRetrieve", createHashMap];
|
||||
}],
|
||||
["getActiveBrowserControl", compileFinal {
|
||||
private _display = uiNamespace getVariable ["RscGarage", displayNull];
|
||||
if (isNull _display) exitWith {
|
||||
@ -40,164 +45,13 @@ GVAR(GarageUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
_screen call ["markReady", [true]];
|
||||
|
||||
_self call ["flushPendingEvents", []];
|
||||
_self call ["sendEvent", ["garage::hydrate", GVAR(GarageSessionService) call ["buildPayload", []], _control]];
|
||||
_self call ["sendEvent", ["garage::hydrate", GVAR(GaragePayloadService) call ["buildPayload", []], _control]];
|
||||
}],
|
||||
["refreshGarage", compileFinal {
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
if (isNull _control) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", ["garage::sync", GVAR(GarageSessionService) call ["buildPayload", []], _control]]
|
||||
}],
|
||||
["handleRetrieveRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _plate = _data getOrDefault ["plate", ""];
|
||||
if (_plate isEqualTo "") exitWith {
|
||||
_self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
|
||||
["message", "Select a stored vehicle to retrieve."]
|
||||
]]];
|
||||
};
|
||||
|
||||
private _garageMap = if (isNil QGVAR(GarageClass)) then {
|
||||
createHashMap
|
||||
} else {
|
||||
GVAR(GarageClass) call ["getGarageState", []]
|
||||
};
|
||||
private _vehicleData = _garageMap getOrDefault [_plate, createHashMap];
|
||||
if (_vehicleData isEqualTo createHashMap) exitWith {
|
||||
_self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
|
||||
["message", "Stored vehicle record could not be found."]
|
||||
]]];
|
||||
};
|
||||
|
||||
private _context = GVAR(GarageSessionService) call ["getContext", []];
|
||||
private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
|
||||
private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player];
|
||||
private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
|
||||
private _blockingVehicles = [];
|
||||
{
|
||||
_blockingVehicles pushBackUnique _x;
|
||||
} forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]);
|
||||
{
|
||||
_blockingVehicles pushBackUnique _x;
|
||||
} forEach (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius]);
|
||||
if (_blockingVehicles isNotEqualTo []) exitWith {
|
||||
_self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
|
||||
["message", "The garage spawn area is blocked."]
|
||||
]]];
|
||||
};
|
||||
|
||||
private _className = _vehicleData getOrDefault ["classname", ""];
|
||||
if (_className isEqualTo "") exitWith {
|
||||
_self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
|
||||
["message", "Stored vehicle record is missing a classname."]
|
||||
]]];
|
||||
};
|
||||
|
||||
private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"];
|
||||
_vehicle setDir _spawnHeading;
|
||||
_vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]);
|
||||
_vehicle setDamage (_vehicleData getOrDefault ["damage", 0]);
|
||||
|
||||
private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
|
||||
private _hitPointNames = _hitPoints getOrDefault ["names", []];
|
||||
private _hitPointValues = _hitPoints getOrDefault ["values", []];
|
||||
for "_index" from 0 to ((count _hitPointNames) - 1) do {
|
||||
_vehicle setHitPointDamage [_hitPointNames param [_index, ""], _hitPointValues param [_index, 0]];
|
||||
};
|
||||
|
||||
_vehicle setVariable ["forge_garage_plate", _plate, true];
|
||||
_vehicle setVariable ["forge_garage_owner_uid", getPlayerUID player, true];
|
||||
|
||||
_self set ["pendingRetrieve", createHashMapFromArray [
|
||||
["plate", _plate],
|
||||
["vehicle", _vehicle]
|
||||
]];
|
||||
|
||||
[SRPC(garage,requestRetrieveVehicle), [getPlayerUID player, _plate]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["handleStoreRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _netId = _data getOrDefault ["netId", ""];
|
||||
if (_netId isEqualTo "") exitWith {
|
||||
_self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [
|
||||
["message", "Select a nearby vehicle to store."]
|
||||
]]];
|
||||
};
|
||||
|
||||
private _vehicle = objectFromNetId _netId;
|
||||
if (isNull _vehicle) exitWith {
|
||||
_self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [
|
||||
["message", "The selected vehicle is no longer available."]
|
||||
]]];
|
||||
};
|
||||
|
||||
if (crew _vehicle isNotEqualTo []) exitWith {
|
||||
_self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [
|
||||
["message", "All crew must exit the vehicle before storing it."]
|
||||
]]];
|
||||
};
|
||||
|
||||
private _rawHitPoints = getAllHitPointsDamage _vehicle;
|
||||
private _hitPointsJson = toJSON (createHashMapFromArray [
|
||||
["names", _rawHitPoints param [0, []]],
|
||||
["selections", _rawHitPoints param [1, []]],
|
||||
["values", _rawHitPoints param [2, []]]
|
||||
]);
|
||||
|
||||
_self set ["pendingStoreVehicle", _vehicle];
|
||||
[SRPC(garage,requestStoreVehicle), [
|
||||
getPlayerUID player,
|
||||
typeOf _vehicle,
|
||||
fuel _vehicle,
|
||||
damage _vehicle,
|
||||
_hitPointsJson
|
||||
]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["handleActionResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
private _action = _payload getOrDefault ["action", ""];
|
||||
private _success = _payload getOrDefault ["success", false];
|
||||
private _message = _payload getOrDefault ["message", "Garage action failed."];
|
||||
|
||||
switch (_action) do {
|
||||
case "retrieve": {
|
||||
private _pendingRetrieve = _self getOrDefault ["pendingRetrieve", createHashMap];
|
||||
private _vehicle = _pendingRetrieve getOrDefault ["vehicle", objNull];
|
||||
|
||||
if (!_success && { !isNull _vehicle }) then {
|
||||
deleteVehicle _vehicle;
|
||||
};
|
||||
|
||||
_self set ["pendingRetrieve", createHashMap];
|
||||
_self call ["sendEvent", [[
|
||||
"garage::retrieve::failure",
|
||||
"garage::retrieve::success"
|
||||
] select _success, createHashMapFromArray [["message", _message]]]];
|
||||
};
|
||||
case "store": {
|
||||
private _vehicle = _self getOrDefault ["pendingStoreVehicle", objNull];
|
||||
|
||||
if (_success && { !isNull _vehicle }) then {
|
||||
deleteVehicle _vehicle;
|
||||
};
|
||||
|
||||
_self set ["pendingStoreVehicle", objNull];
|
||||
_self call ["sendEvent", [[
|
||||
"garage::store::failure",
|
||||
"garage::store::success"
|
||||
] select _success, createHashMapFromArray [["message", _message]]]];
|
||||
};
|
||||
};
|
||||
|
||||
[] spawn {
|
||||
sleep 0.05;
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||
};
|
||||
};
|
||||
_self call ["sendEvent", ["garage::sync", GVAR(GaragePayloadService) call ["buildPayload", []], _control]]
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -1,50 +1,45 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initVGClass.sqf
|
||||
* File: fnc_initVGRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-16
|
||||
* Last Update: 2026-02-13
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the Virtual Garage class for managing player garage unlocks.
|
||||
* Provides methods for syncing, saving, and applying virtual items to BIS Garage.
|
||||
* Initializes the virtual garage repository for BIS virtual garage state.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* vGarage class object [HASHMAP OBJECT]
|
||||
* Virtual garage repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initVGClass;
|
||||
* call forge_client_garage_fnc_initVGRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(VGBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "VGBaseClass"],
|
||||
GVAR(VGRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "VGRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
GVAR(isPreLoaded) = false;
|
||||
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["vGarage", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
private _vGarage = _self get "vGarage";
|
||||
[SRPC(garage,requestInitVG), [_uid]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
[SRPC(garage,requestInitVG), [_uid, _vGarage]] call CFUNC(serverEvent);
|
||||
|
||||
systemChat format ["VGarage loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:VGarage] VGarage Class Initialized!";
|
||||
systemChat format ["VGarage loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:VGarage] Repository Initialized!";
|
||||
}],
|
||||
["save", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(garage,requestSaveVG), [_uid]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", compileFinal {
|
||||
@ -55,7 +50,6 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
{
|
||||
_vGarage set [_x, _y];
|
||||
|
||||
switch (_x) do {
|
||||
case "cars": { _self call ["apply", ["cars"]]; };
|
||||
case "armor": { _self call ["apply", ["armor"]]; };
|
||||
@ -68,9 +62,8 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [
|
||||
} forEach _data;
|
||||
|
||||
_self set ["vGarage", _vGarage];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:VGarage] Sync completed";
|
||||
diag_log "[FORGE:Client:VGarage] Repository sync completed";
|
||||
}],
|
||||
["get", compileFinal {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
@ -83,7 +76,6 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _vehicles = _self call ["get", [_key, []]];
|
||||
private _appliedVehicles = [];
|
||||
|
||||
{
|
||||
_appliedVehicles append [getText (configFile >> "CfgVehicles" >> _x >> "model"), [configFile >> "CfgVehicles" >> _x]];
|
||||
} forEach _vehicles;
|
||||
@ -100,5 +92,5 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(VGClass) = createHashMapObject [GVAR(VGBaseClass)];
|
||||
GVAR(VGClass)
|
||||
GVAR(VGRepository) = createHashMapObject [GVAR(VGRepositoryBaseClass)];
|
||||
GVAR(VGRepository)
|
||||
@ -89,7 +89,7 @@ if !(GVAR(isPreLoaded)) then {
|
||||
private _nearVehicles = FORGE_VehSpawnPos nearEntities [["Car", "Tank", "Air", "Ship"], 5];
|
||||
if (_nearVehicles isNotEqualTo []) exitWith {
|
||||
private _params = ["warning", "Virtual Garage", "Vehicle spawn position is blocked. Please move the vehicle before accessing the garage.", 3000];
|
||||
EGVAR(notifications,NotificationClass) call ["create", _params];
|
||||
EGVAR(notifications,NotificationService) call ["create", _params];
|
||||
};
|
||||
|
||||
["Open", true] call BFUNC(garage);
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
PREP(initLockerClass);
|
||||
PREP(initVAClass);
|
||||
PREP(initRepository);
|
||||
PREP(initVARepository);
|
||||
|
||||
@ -1,48 +1,48 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(LockerClass)) then { call FUNC(initLockerClass); };
|
||||
if (isNil QGVAR(VAClass)) then { call FUNC(initVAClass); };
|
||||
if (isNil QGVAR(LockerRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(VARepository)) then { call FUNC(initVARepository); };
|
||||
|
||||
[QGVAR(initLocker), {
|
||||
GVAR(LockerClass) call ["init", []];
|
||||
GVAR(LockerRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitLocker), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(LockerClass) call ["sync", [_data]];
|
||||
GVAR(LockerRepository) call ["sync", [_data]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncLocker), {
|
||||
params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(LockerClass) call ["sync", [_data, _jip]];
|
||||
GVAR(LockerRepository) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(initVA), {
|
||||
GVAR(VAClass) call ["init", []];
|
||||
GVAR(VARepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitVA), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(VAClass) call ["sync", [_data]];
|
||||
GVAR(VARepository) call ["sync", [_data]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncVA), {
|
||||
params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(VAClass) call ["sync", [_data, _jip]];
|
||||
GVAR(VARepository) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(garage,GarageClass) get "isLoaded";
|
||||
EGVAR(garage,GarageRepository) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initLocker), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
[{
|
||||
GVAR(LockerClass) get "isLoaded";
|
||||
GVAR(LockerRepository) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initVA), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
@ -1,31 +1,29 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initLockerClass.sqf
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-17
|
||||
* Last Update: 2026-02-13
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the Locker class for managing player locker items.
|
||||
* Provides methods for syncing, saving, and applying locker items to the player's locker.
|
||||
* Initializes the locker repository for managing player locker items.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Locker class object [HASHMAP OBJECT]
|
||||
* Locker repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_locker_fnc_initLockerClass
|
||||
* call forge_client_locker_fnc_initRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "LockerBaseClass"],
|
||||
GVAR(LockerRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "LockerRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
_self set ["locker", createHashMap];
|
||||
@ -34,9 +32,10 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _uid = _self get "uid";
|
||||
|
||||
[SRPC(locker,requestInitLocker), [_uid]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
systemChat format ["Locker loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Locker] Locker Class Initialized!";
|
||||
systemChat format ["Locker loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:Locker] Locker Repository Initialized!";
|
||||
}],
|
||||
["get", compileFinal {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
@ -83,8 +82,8 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _cfgWeapons = configFile >> "CfgWeapons" >> _containerClass;
|
||||
private _itemInfoType = getNumber (_cfgWeapons >> "ItemInfo" >> "type");
|
||||
private _isBackpack = isClass _cfgVehicles;
|
||||
private _isUniform = isClass _cfgWeapons && {_itemInfoType == TYPE_UNIFORM};
|
||||
private _isVest = isClass _cfgWeapons && {_itemInfoType == TYPE_VEST};
|
||||
private _isUniform = isClass _cfgWeapons && { _itemInfoType == TYPE_UNIFORM };
|
||||
private _isVest = isClass _cfgWeapons && { _itemInfoType == TYPE_VEST };
|
||||
|
||||
if (!_isBackpack && !_isVest && !_isUniform) then { continue; };
|
||||
|
||||
@ -141,7 +140,6 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _weaponItems = weaponsItemsCargo _container;
|
||||
{
|
||||
// private _weapon = _x param [0, ""];
|
||||
private _muzzle = _x param [1, ""];
|
||||
private _pointer = _x param [2, ""];
|
||||
private _optic = _x param [3, ""];
|
||||
@ -149,7 +147,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
private _underbarrel = _x param [5, ""];
|
||||
private _bipod = _x param [6, ""];
|
||||
private _secondaryMag = _x param [7, ["", 0]];
|
||||
private _attachments = [_muzzle, _pointer, _optic, _underbarrel, _bipod] select {(_x isEqualType "") && {_x != ""}};
|
||||
private _attachments = [_muzzle, _pointer, _optic, _underbarrel, _bipod] select { (_x isEqualType "") && { _x != "" } };
|
||||
{
|
||||
private _existing = _locker getOrDefault [_x, createHashMap];
|
||||
private _existingCount = _existing getOrDefault ["amount", 0];
|
||||
@ -162,7 +160,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
} forEach _attachments;
|
||||
|
||||
if (_primaryMag isNotEqualTo ["", 0]) then {
|
||||
_primaryMag params ["_magClass", "_ammoCount"]; // TODO: Add ammo count to locker
|
||||
_primaryMag params ["_magClass", "_ammoCount"];
|
||||
if (_magClass != "") then {
|
||||
private _existing = _locker getOrDefault [_magClass, createHashMap];
|
||||
private _existingCount = _existing getOrDefault ["amount", 0];
|
||||
@ -176,7 +174,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
};
|
||||
|
||||
if (_secondaryMag isNotEqualTo ["", 0]) then {
|
||||
_secondaryMag params ["_magClass", "_ammoCount"]; // TODO: Add ammo count to locker
|
||||
_secondaryMag params ["_magClass", "_ammoCount"];
|
||||
if (_magClass != "") then {
|
||||
private _existing = _locker getOrDefault [_magClass, createHashMap];
|
||||
private _existingCount = _existing getOrDefault ["amount", 0];
|
||||
@ -204,7 +202,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
_locker addEventHandler ["ContainerOpened", {
|
||||
params ["_container", "_unit"];
|
||||
|
||||
private _index = GVAR(LockerClass) get "locker";
|
||||
private _index = GVAR(LockerRepository) get "locker";
|
||||
|
||||
clearBackpackCargo _container;
|
||||
clearItemCargo _container;
|
||||
@ -227,7 +225,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
if (count _index > 25) then {
|
||||
private _params = ["warning", "Over Capacity", "Locker has more then 25 items, please remove some items", 3000];
|
||||
GVAR(NotificationClass) call ["create", _params];
|
||||
GVAR(NotificationService) call ["create", _params];
|
||||
};
|
||||
}];
|
||||
|
||||
@ -235,17 +233,17 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
params ["_container", "_unit"];
|
||||
|
||||
private _newLocker = createHashMap;
|
||||
_newLocker = GVAR(LockerClass) call ["getCargo", [_container, _newLocker]];
|
||||
_newLocker = GVAR(LockerClass) call ["getContainerItems", [_container, _newLocker]];
|
||||
_newLocker = GVAR(LockerClass) call ["getAttachments", [_container, _newLocker]];
|
||||
_newLocker = GVAR(LockerRepository) call ["getCargo", [_container, _newLocker]];
|
||||
_newLocker = GVAR(LockerRepository) call ["getContainerItems", [_container, _newLocker]];
|
||||
_newLocker = GVAR(LockerRepository) call ["getAttachments", [_container, _newLocker]];
|
||||
|
||||
private _uid = getPlayerUID _unit;
|
||||
[SRPC(locker,requestOverrideLocker), [_uid, _newLocker]] call CFUNC(serverEvent);
|
||||
GVAR(LockerClass) set ["locker", _newLocker];
|
||||
GVAR(LockerRepository) set ["locker", _newLocker];
|
||||
|
||||
if (count _newLocker > 25) then {
|
||||
private _params = ["warning", "Over Capacity", "Locker has more then 25 items, please remove some items", 3000];
|
||||
GVAR(NotificationClass) call ["create", _params];
|
||||
GVAR(NotificationService) call ["create", _params];
|
||||
};
|
||||
}];
|
||||
}],
|
||||
@ -295,5 +293,5 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(LockerClass) = createHashMapObject [GVAR(LockerBaseClass)];
|
||||
GVAR(LockerClass)
|
||||
GVAR(LockerRepository) = createHashMapObject [GVAR(LockerRepositoryBaseClass)];
|
||||
GVAR(LockerRepository)
|
||||
@ -1,31 +1,29 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_init.sqf
|
||||
* File: fnc_initVARepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-16
|
||||
* Last Update: 2026-02-13
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the Virtual Arsenal class for managing player arsenal unlocks.
|
||||
* Provides methods for syncing, saving, and applying virtual items to BIS Arsenal.
|
||||
* Initializes the virtual arsenal repository for managing player arsenal unlocks.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* vArsenal class object [HASHMAP OBJECT]
|
||||
* Virtual arsenal repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_locker_fnc_init;
|
||||
* call forge_client_locker_fnc_initVARepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(VABaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "VABaseClass"],
|
||||
GVAR(VARepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "VARepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["vArsenal", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
@ -34,9 +32,10 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [
|
||||
private _uid = _self get "uid";
|
||||
FORGE_Locker_Box = "ReammoBox_F" createVehicleLocal [0, 0, -999];
|
||||
[SRPC(locker,requestInitVA), [_uid]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
systemChat format ["VArsenal loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:VArsenal] VArsenal Class Initialized!";
|
||||
systemChat format ["VArsenal loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:VArsenal] Repository Initialized!";
|
||||
}],
|
||||
["save", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
@ -91,5 +90,5 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(VAClass) = createHashMapObject [GVAR(VABaseClass)];
|
||||
GVAR(VAClass)
|
||||
GVAR(VARepository) = createHashMapObject [GVAR(VARepositoryBaseClass)];
|
||||
GVAR(VARepository)
|
||||
@ -1,3 +1,3 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initNotificationClass);
|
||||
PREP(initService);
|
||||
PREP(openUI);
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
[{
|
||||
EGVAR(actor,ActorClass) get "isLoaded";
|
||||
EGVAR(locker,VARepository) get "isLoaded";
|
||||
}, {
|
||||
("NotificationHudLayer" call BFUNC(rscLayer)) cutRsc ["RscNotifications", "PLAIN"];
|
||||
call FUNC(openUI);
|
||||
if (isNil QGVAR(NotificationClass)) then { call FUNC(initNotificationClass); };
|
||||
if (isNil QGVAR(NotificationService)) then { call FUNC(initService); };
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
[QGVAR(recieveNotification), {
|
||||
params [["_type", "", [""]], ["_title", "", [""]], ["_content", "", [""]], ["_duration", 4000, [4000]]];
|
||||
|
||||
playSound QGVAR(notify);
|
||||
GVAR(NotificationClass) call ["create", [_type, _title, _content, _duration]];
|
||||
GVAR(NotificationService) call ["create", [_type, _title, _content, _duration]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
|
||||
@ -32,7 +32,7 @@ diag_log format ["[FORGE:Client:Notifications] Handling UI event: %1 with data:
|
||||
|
||||
switch (_event) do {
|
||||
case "notifications::ready": {
|
||||
GVAR(NotificationClass) call ["init", []];
|
||||
GVAR(NotificationService) call ["init", []];
|
||||
};
|
||||
default { hint format ["[FORGE:Client:Notifications] Unhandled event: %1", _event]; };
|
||||
};
|
||||
|
||||
@ -1,29 +1,27 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initNotificationClass.sqf
|
||||
* File: fnc_initService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-01-30
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the notification class for managing player notifications.
|
||||
* Provides methods for creating and displaying notifications.
|
||||
* Initializes the notification service for client notification display.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Notification class object [HASHMAP OBJECT]
|
||||
* Notification service object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_notifications_fnc_initNotificationClass
|
||||
* call forge_client_notifications_fnc_initService;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(NotificationClass) = createHashMapObject [[
|
||||
["#type", "INotificationClass"],
|
||||
GVAR(NotificationService) = createHashMapObject [[
|
||||
["#type", "INotificationService"],
|
||||
["#create", {
|
||||
private _display = uiNamespace getVariable ["RscNotifications", nil];
|
||||
private _control = _display displayCtrl 1004;
|
||||
@ -37,8 +35,8 @@ GVAR(NotificationClass) = createHashMapObject [[
|
||||
_self call ["create", _params];
|
||||
_self set ["isLoaded", true];
|
||||
|
||||
systemChat format ["Notifications loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Notifications] Notification Class Initialized!";
|
||||
systemChat format ["Notifications loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:Notifications] Notification Service Initialized!";
|
||||
}],
|
||||
["create", {
|
||||
params [["_type", "", ["info"]], ["_title", "", [""]], ["_content", "", [""]], ["_duration", 4000]];
|
||||
@ -55,4 +53,4 @@ GVAR(NotificationClass) = createHashMapObject [[
|
||||
}]
|
||||
]];
|
||||
|
||||
GVAR(NotificationClass)
|
||||
GVAR(NotificationService)
|
||||
@ -1,4 +1,4 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initClass);
|
||||
PREP(initRepository);
|
||||
PREP(initUIBridge);
|
||||
PREP(openUI);
|
||||
|
||||
@ -1,26 +1,31 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(OrgClass)) then { call FUNC(initClass); };
|
||||
if (isNil QGVAR(OrgRepository)) then { call FUNC(initRepository); };
|
||||
if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(initOrg), {
|
||||
GVAR(OrgClass) call ["init", []];
|
||||
GVAR(OrgRepository) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitOrg), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(OrgClass) call ["sync", [_data, true]];
|
||||
GVAR(OrgUIBridge) call ["refreshPortal", []];
|
||||
GVAR(OrgRepository) call ["markLoaded", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncOrg), {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(OrgClass) call ["sync", [_data, _jip]];
|
||||
GVAR(OrgRepository) call ["markLoaded", []];
|
||||
GVAR(OrgUIBridge) call ["refreshPortal", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseHydrateOrg), {
|
||||
params [["_payload", createHashMap, [createHashMap]], ["_bridgeEvent", "org::sync", [""]]];
|
||||
|
||||
GVAR(OrgUIBridge) call ["handleHydrateResponse", [_payload, _bridgeEvent]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCreateOrg), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
@ -46,7 +51,7 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(actor,ActorClass) get "isLoaded";
|
||||
EGVAR(locker,VARepository) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initOrg), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_handleUIEvents.sqf
|
||||
* Author: IDSolutions
|
||||
* Handles the UI events.
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Handles the org UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
* 0: [CONTROL] - The control that triggered the event
|
||||
* 1: [BOOL] - Whether the event is from a confirm dialog
|
||||
* 2: [STRING] - The message containing the event data
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
* UI events handled [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_org_fnc_handleUIEvents;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-02-13
|
||||
* Last Update: 2026-02-13
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the org class.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Org class object [HASHMAP OBJECT]
|
||||
*
|
||||
* Examples:
|
||||
* call forge_client_org_fnc_initClass
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "OrgBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["org", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
|
||||
private _org = createHashMap;
|
||||
_org set ["id", ""];
|
||||
_org set ["owner", ""];
|
||||
_org set ["name", ""];
|
||||
_org set ["funds", 0];
|
||||
_org set ["reputation", 0];
|
||||
_org set ["credit_lines", createHashMap];
|
||||
_org set ["assets", createHashMap];
|
||||
_org set ["fleet", createHashMap];
|
||||
_org set ["members", createHashMap];
|
||||
|
||||
_self set ["org", _org];
|
||||
}],
|
||||
["init", compileFinal {
|
||||
private _uid = _self get "uid";
|
||||
private _org = _self get "org";
|
||||
|
||||
[SRPC(org,requestInitOrg), [_uid, _org]] call CFUNC(serverEvent);
|
||||
|
||||
systemChat format ["Org loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Org] Org Class Initialized!";
|
||||
}],
|
||||
["save", compileFinal {
|
||||
params [["_sync", false, [false]]];
|
||||
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(org,requestSaveOrg), [_uid, _sync]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
private _org = _self get "org";
|
||||
|
||||
{ _org set [_x, _y]; } forEach _data;
|
||||
_self set ["org", _org];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Org] Sync completed";
|
||||
}],
|
||||
["buildPortalPayload", compileFinal {
|
||||
private _orgData = _self get "org";
|
||||
|
||||
private _name = _orgData get "name";
|
||||
private _id = _orgData get "id";
|
||||
private _ownerUid = _orgData get "owner";
|
||||
private _funds = _orgData get "funds";
|
||||
private _reputation = _orgData get "reputation";
|
||||
private _creditLinesRaw = _orgData getOrDefault ["credit_lines", createHashMap];
|
||||
private _assetsRaw = _orgData get "assets";
|
||||
private _fleetRaw = _orgData get "fleet";
|
||||
private _membersRaw = _orgData get "members";
|
||||
private _isDefaultOrg = (_orgData getOrDefault ["default", false])
|
||||
|| {toLower _id isEqualTo "default"}
|
||||
|| {toLower _ownerUid isEqualTo "server"};
|
||||
|
||||
private _playerName = name player;
|
||||
private _playerUid = getPlayerUID player;
|
||||
private _playerVar = vehicleVarName player;
|
||||
private _sessionRole = "Member";
|
||||
private _sessionIsCeo = _isDefaultOrg && {_playerVar isEqualTo "ceo"};
|
||||
private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server");
|
||||
|
||||
private _membersList = [];
|
||||
{
|
||||
private _memberData = _y;
|
||||
private _memberName = _memberData getOrDefault ["name", "Unknown"];
|
||||
private _memberUid = _memberData getOrDefault ["uid", ""];
|
||||
|
||||
if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then { _ownerName = _memberName; };
|
||||
if (_memberUid isEqualTo _playerUid) then { _sessionRole = "Member"; };
|
||||
|
||||
_membersList pushBack (createHashMapFromArray [
|
||||
["uid", _memberUid],
|
||||
["name", _memberName]
|
||||
]);
|
||||
} forEach _membersRaw;
|
||||
|
||||
if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _playerUid }) then { _ownerName = _playerName; };
|
||||
if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; };
|
||||
if (_ownerUid isEqualTo _playerUid) then { _sessionRole = "Leader"; };
|
||||
|
||||
private _assetsList = [];
|
||||
{
|
||||
private _assetData = _y;
|
||||
_assetsList pushBack (createHashMapFromArray [
|
||||
["name", _assetData getOrDefault ["name", "Unknown Asset"]],
|
||||
["type", _assetData getOrDefault ["type", "items"]],
|
||||
["quantity", str (_assetData getOrDefault ["quantity", 0])]
|
||||
]);
|
||||
} forEach _assetsRaw;
|
||||
|
||||
private _fleetList = [];
|
||||
{
|
||||
private _vehicleData = _y;
|
||||
_fleetList pushBack (createHashMapFromArray [
|
||||
["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]],
|
||||
["type", _vehicleData getOrDefault ["type", "other"]],
|
||||
["status", _vehicleData getOrDefault ["status", "Unknown"]],
|
||||
["damage", _vehicleData getOrDefault ["damage", "0%"]]
|
||||
]);
|
||||
} forEach _fleetRaw;
|
||||
|
||||
private _creditLinesList = [];
|
||||
{
|
||||
private _creditLineData = _y;
|
||||
_creditLinesList pushBack (createHashMapFromArray [
|
||||
["uid", _creditLineData getOrDefault ["uid", _x]],
|
||||
["member", _creditLineData getOrDefault ["name", "Unknown Member"]],
|
||||
["amount", _creditLineData getOrDefault ["amount", 0]]
|
||||
]);
|
||||
} forEach _creditLinesRaw;
|
||||
|
||||
createHashMapFromArray [
|
||||
["session", createHashMapFromArray [
|
||||
["actorName", _playerName],
|
||||
["actorUid", _playerUid],
|
||||
["role", _sessionRole],
|
||||
["ceo", _sessionIsCeo]
|
||||
]],
|
||||
["portalData", createHashMapFromArray [
|
||||
["org", createHashMapFromArray [
|
||||
["name", _name],
|
||||
["tag", _id],
|
||||
["owner", _ownerName],
|
||||
["ownerUid", _ownerUid],
|
||||
["isDefault", _isDefaultOrg]
|
||||
]],
|
||||
["funds", _funds],
|
||||
["reputation", _reputation],
|
||||
["creditLines", _creditLinesList],
|
||||
["members", _membersList],
|
||||
["fleet", _fleetList],
|
||||
["assets", _assetsList],
|
||||
["activity", []]
|
||||
]]
|
||||
]
|
||||
}],
|
||||
["get", compileFinal {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
private _org = _self get "org";
|
||||
_org getOrDefault [_key, _default];
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(OrgClass) = createHashMapObject [GVAR(OrgBaseClass)];
|
||||
GVAR(OrgClass)
|
||||
44
arma/client/addons/org/functions/fnc_initRepository.sqf
Normal file
44
arma/client/addons/org/functions/fnc_initRepository.sqf
Normal file
@ -0,0 +1,44 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initRepository.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the org repository for client org lifecycle state.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* Org repository object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_org_fnc_initRepository;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(OrgRepositoryBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "OrgRepositoryBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["uid", getPlayerUID player];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", compileFinal {
|
||||
[SRPC(org,requestInitOrg), [getPlayerUID player]] call CFUNC(serverEvent);
|
||||
_self set ["lastSave", time];
|
||||
|
||||
systemChat format ["Org loaded for %1", name player];
|
||||
diag_log "[FORGE:Client:Org] Org Repository Initialized!";
|
||||
}],
|
||||
["markLoaded", compileFinal {
|
||||
if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; };
|
||||
true
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(OrgRepository) = createHashMapObject [GVAR(OrgRepositoryBaseClass)];
|
||||
GVAR(OrgRepository)
|
||||
@ -50,20 +50,42 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
_self call ["setActiveBrowserControl", [_control]];
|
||||
_control
|
||||
}],
|
||||
["hasOpenScreen", compileFinal {
|
||||
private _screen = _self call ["getScreen", []];
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
|
||||
!(isNull _control) && { _screen call ["isReady", []] }
|
||||
}],
|
||||
["requestHydrate", compileFinal {
|
||||
params [["_bridgeEvent", "org::sync", [""]]];
|
||||
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
private _event = _bridgeEvent;
|
||||
if !(_event in ["org::login::success", "org::create::success", "org::sync"]) then {
|
||||
_event = "org::sync";
|
||||
};
|
||||
|
||||
[SRPC(org,requestHydrateOrg), [getPlayerUID player, _event]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleHydrateResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]], ["_bridgeEvent", "org::sync", [""]]];
|
||||
|
||||
if !(_self call ["hasOpenScreen", []]) exitWith { false };
|
||||
|
||||
private _event = _bridgeEvent;
|
||||
if !(_event in ["org::login::success", "org::create::success", "org::sync"]) then {
|
||||
_event = "org::sync";
|
||||
};
|
||||
|
||||
_self call ["sendEvent", [_event, _payload, _self call ["getActiveBrowserControl", []]]]
|
||||
}],
|
||||
["handleLoginRequest", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]]];
|
||||
|
||||
private _orgData = GVAR(OrgClass) get "org";
|
||||
private _orgId = _orgData getOrDefault ["id", ""];
|
||||
private _orgName = _orgData getOrDefault ["name", ""];
|
||||
|
||||
if (_orgId isEqualTo "" && { _orgName isEqualTo "" }) exitWith {
|
||||
_self call ["sendEvent", ["org::login::failure", createHashMapFromArray [
|
||||
["message", "No organization data is available for this player."]
|
||||
], _control]];
|
||||
};
|
||||
|
||||
_self call ["sendEvent", ["org::login::success", GVAR(OrgClass) call ["buildPortalPayload", []], _control]];
|
||||
_self call ["setActiveBrowserControl", [_control]];
|
||||
_self call ["requestHydrate", ["org::login::success"]];
|
||||
}],
|
||||
["handleCreateRequest", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]];
|
||||
@ -91,11 +113,11 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
], _control]];
|
||||
};
|
||||
|
||||
private _orgData = _payload getOrDefault ["org", createHashMap];
|
||||
GVAR(OrgClass) call ["sync", [_orgData, true]];
|
||||
if !(isNull _control) then {
|
||||
_self call ["setActiveBrowserControl", [_control]];
|
||||
};
|
||||
|
||||
if (isNull _control) exitWith {};
|
||||
_self call ["sendEvent", ["org::create::success", GVAR(OrgClass) call ["buildPortalPayload", []], _control]];
|
||||
_self call ["requestHydrate", ["org::create::success"]];
|
||||
}],
|
||||
["handleDisbandResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
@ -155,10 +177,7 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
[SRPC(org,requestAssignCreditLine), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["refreshPortal", compileFinal {
|
||||
private _control = _self call ["getActiveBrowserControl", []];
|
||||
if (isNull _control) exitWith { false };
|
||||
|
||||
_self call ["sendEvent", ["org::sync", GVAR(OrgClass) call ["buildPortalPayload", []], _control]]
|
||||
_self call ["requestHydrate", ["org::sync"]]
|
||||
}]
|
||||
];
|
||||
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_openUI.sqf
|
||||
* Author: IDSolutions
|
||||
* Opens the player interaction interface.
|
||||
* Date: 2026-03-27
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Opens the org UI.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
* UI opened [BOOL]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_org_fnc_openUI;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
private _display = createDialog ["RscOrg", true];
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -136,8 +136,18 @@
|
||||
|
||||
OrgPortal.store.setCreditLines((currentLines) => {
|
||||
const nextLine = {
|
||||
amount: payloadData.amount || 0,
|
||||
amount: payloadData.availableAmount || payloadData.amount || 0,
|
||||
amountDue: payloadData.amountDue || 0,
|
||||
approvedAmount:
|
||||
payloadData.approvedAmount ||
|
||||
payloadData.availableAmount ||
|
||||
payloadData.amount ||
|
||||
0,
|
||||
availableAmount:
|
||||
payloadData.availableAmount || payloadData.amount || 0,
|
||||
interestRate: payloadData.interestRate || 0.1,
|
||||
member: payloadData.memberName || "",
|
||||
outstandingPrincipal: payloadData.outstandingPrincipal || 0,
|
||||
uid: payloadData.memberUid || "",
|
||||
};
|
||||
const matchIndex = currentLines.findIndex(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user