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:
Jacob Schmidt 2026-04-02 16:50:38 -05:00
parent 7a8ca6b237
commit ff7ff0c4e5
246 changed files with 24542 additions and 3164 deletions

View File

@ -1,3 +1,3 @@
PREP(handleUIEvents);
PREP(initActorClass);
PREP(initRepository);
PREP(openUI);

View File

@ -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);

View File

@ -1,3 +1,2 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@ -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); };

View File

@ -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)

View File

@ -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",

View File

@ -1,5 +1,4 @@
PREP(handleUIEvents);
PREP(initClass);
PREP(initSessionService);
PREP(initRepository);
PREP(initUIBridge);
PREP(openUI);

View File

@ -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);

View File

@ -1,3 +1,2 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@ -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];
};

View File

@ -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)

View 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)

View File

@ -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)

View File

@ -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

View File

@ -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);
},

View File

@ -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,

View File

@ -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",
),
),
),
);
}

View File

@ -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,

View File

@ -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("");

View File

@ -0,0 +1 @@
forge\forge_client\addons\cad

View 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));
};
};

View 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.

View File

@ -0,0 +1,5 @@
PREP(handleUIEvents);
PREP(initRepository);
PREP(initUIBridge);
PREP(initUI);
PREP(openUI);

View 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);

View File

@ -0,0 +1,5 @@
#include "script_component.hpp"
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;

View File

@ -0,0 +1 @@
#include "script_component.hpp"

View 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"

View 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

View 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)

View 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

View 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)

View 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

View 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"

View File

@ -0,0 +1,6 @@
// Control types
#define CT_STATIC 0
#define CT_MAP 100
class RscText;
class RscMapControl;

View 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};
};
};
};

View 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>

View 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}

View File

@ -0,0 +1 @@
window.CADBottombar=window.CADBottombar||{init:()=>!0},window.CADBottombar.init();

View 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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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)};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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}

View 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();

File diff suppressed because one or more lines are too long

View 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>

View 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>

View 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>

View File

@ -0,0 +1,7 @@
window.CADBottombar = window.CADBottombar || {
init() {
return true;
},
};
window.CADBottombar.init();

View 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>

View 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}.`;
},
};

View 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();

View 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";
},
};

View 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"
>
&#9881;
</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();
},
};

View 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;
},
};

View 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>

File diff suppressed because it is too large Load Diff

View 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);
}

View 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);
}

View 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);
}
}

View 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);
}
}

View 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;
}

View 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>

View 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();

View 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: {},
};

View File

@ -1,3 +1,2 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@ -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);

View File

@ -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);

View File

@ -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": {

View 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)

View 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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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]]
}]
];

View File

@ -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)

View File

@ -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);

View File

@ -1,2 +1,2 @@
PREP(initLockerClass);
PREP(initVAClass);
PREP(initRepository);
PREP(initVARepository);

View File

@ -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);

View File

@ -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]]];
@ -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, ""];
@ -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)

View File

@ -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)

View File

@ -1,3 +1,3 @@
PREP(handleUIEvents);
PREP(initNotificationClass);
PREP(initService);
PREP(openUI);

View File

@ -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);

View File

@ -1,3 +1,2 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@ -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]; };
};

View File

@ -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)

View File

@ -1,4 +1,4 @@
PREP(handleUIEvents);
PREP(initClass);
PREP(initRepository);
PREP(initUIBridge);
PREP(openUI);

View File

@ -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);

View File

@ -1,3 +1,2 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@ -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"];

View File

@ -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)

View 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)

View File

@ -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"]]
}]
];

View File

@ -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

View File

@ -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