Implement interactive garage UI with service-based client bridge
- replace placeholder garage interaction with real UI open flow - add catalog/session/UI bridge services for hydrate, sync, store, and retrieve actions - migrate garage web UI bundle to new app shell/runtime structure - align org/store function naming with shared init and UI bridge patterns
This commit is contained in:
parent
e15d4b3066
commit
bdc1e36e63
@ -36,7 +36,7 @@ switch (_event) do {
|
||||
case "actor::open::atm": { [true] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
|
||||
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
|
||||
case "actor::open::garage": { hint "Garage interaction is not yet implemented."; };
|
||||
case "actor::open::garage": { [] spawn EFUNC(garage,openUI); };
|
||||
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
|
||||
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
|
||||
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
PREP(initGarageClass);
|
||||
PREP(handleUIEvents);
|
||||
PREP(initCatalogService);
|
||||
PREP(initClass);
|
||||
PREP(initSessionService);
|
||||
PREP(initUIBridge);
|
||||
PREP(initVGClass);
|
||||
PREP(openUI);
|
||||
PREP(openVG);
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(GarageClass)) then { call FUNC(initGarageClass); };
|
||||
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(GarageUIBridge)) then { call FUNC(initUIBridge); };
|
||||
if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); };
|
||||
|
||||
[QGVAR(initGarage), {
|
||||
@ -11,12 +14,26 @@ if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); };
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(GarageClass) call ["sync", [_data]];
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncGarage), {
|
||||
params [["_data", createHashMap, [createHashMap, []]]];
|
||||
|
||||
GVAR(GarageClass) call ["sync", [_data]];
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseGarageAction), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleActionResponse", [_payload]];
|
||||
};
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(initVG), {
|
||||
|
||||
@ -8,6 +8,7 @@ class CfgPatches {
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_common",
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
@ -17,3 +18,5 @@ class CfgPatches {
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "ui\RscCommon.hpp"
|
||||
#include "ui\RscGarage.hpp"
|
||||
|
||||
@ -21,3 +21,46 @@
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_handleUIEvents;
|
||||
*/
|
||||
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
private _alert = fromJSON _message;
|
||||
private _event = _alert get "event";
|
||||
private _data = _alert get "data";
|
||||
|
||||
diag_log format ["[FORGE:Client:Garage] Handling UI event: %1 with data: %2", _event, _data];
|
||||
|
||||
switch (_event) do {
|
||||
case "garage::close": {
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleClose", []];
|
||||
};
|
||||
|
||||
closeDialog 1;
|
||||
};
|
||||
case "garage::ready": {
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleReady", [_control, _data]];
|
||||
};
|
||||
};
|
||||
case "garage::vehicle::retrieve::request": {
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleRetrieveRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "garage::vehicle::store::request": {
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["handleStoreRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "garage::refresh": {
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["refreshGarage", []];
|
||||
};
|
||||
};
|
||||
default {
|
||||
hint format ["Unhandled garage UI event: %1", _event];
|
||||
};
|
||||
};
|
||||
|
||||
true;
|
||||
|
||||
160
arma/client/addons/garage/functions/fnc_initCatalogService.sqf
Normal file
160
arma/client/addons/garage/functions/fnc_initCatalogService.sqf
Normal file
@ -0,0 +1,160 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initCatalogService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-14
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the garage catalog service for vehicle metadata and UI-friendly shaping.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GarageCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "GarageCatalogServiceBaseClass"],
|
||||
["resolveCategory", compileFinal {
|
||||
params [["_className", "", [""]]];
|
||||
|
||||
if (_className isEqualTo "") exitWith { "other" };
|
||||
|
||||
switch (true) do {
|
||||
case (_className isKindOf ["Car", configFile >> "CfgVehicles"]): { "car" };
|
||||
case (_className isKindOf ["Tank", configFile >> "CfgVehicles"]): { "armor" };
|
||||
case (_className isKindOf ["Helicopter", configFile >> "CfgVehicles"]): { "air" };
|
||||
case (_className isKindOf ["Plane", configFile >> "CfgVehicles"]): { "air" };
|
||||
case (_className isKindOf ["Ship", configFile >> "CfgVehicles"]): { "naval" };
|
||||
default { "other" };
|
||||
}
|
||||
}],
|
||||
["resolveDisplayName", compileFinal {
|
||||
params [["_className", "", [""]]];
|
||||
|
||||
private _displayName = getText (configFile >> "CfgVehicles" >> _className >> "displayName");
|
||||
if (_displayName isEqualTo "") then {
|
||||
_displayName = _className;
|
||||
};
|
||||
|
||||
_displayName
|
||||
}],
|
||||
["resolvePicture", compileFinal {
|
||||
params [["_className", "", [""]]];
|
||||
|
||||
private _picture = getText (configFile >> "CfgVehicles" >> _className >> "editorPreview");
|
||||
if (_picture isEqualTo "") then {
|
||||
_picture = getText (configFile >> "CfgVehicles" >> _className >> "picture");
|
||||
};
|
||||
|
||||
_picture
|
||||
}],
|
||||
["buildHitPointRows", compileFinal {
|
||||
params [["_hitPoints", createHashMap, [createHashMap]]];
|
||||
|
||||
private _rows = [];
|
||||
private _names = _hitPoints getOrDefault ["names", []];
|
||||
private _selections = _hitPoints getOrDefault ["selections", []];
|
||||
private _values = _hitPoints getOrDefault ["values", []];
|
||||
private _count = count _names;
|
||||
|
||||
for "_index" from 0 to (_count - 1) do {
|
||||
private _rowName = _names param [_index, ""];
|
||||
_rows pushBack (createHashMapFromArray [
|
||||
["name", _rowName],
|
||||
["selection", _selections param [_index, ""]],
|
||||
["value", _values param [_index, 0]]
|
||||
]);
|
||||
};
|
||||
|
||||
_rows
|
||||
}],
|
||||
["resolveHealth", compileFinal {
|
||||
params [["_damage", 0, [0]], ["_hitPointRows", [], [[]]]];
|
||||
|
||||
private _worstHitPoint = 0;
|
||||
{
|
||||
private _value = _x getOrDefault ["value", 0];
|
||||
if (_value > _worstHitPoint) then {
|
||||
_worstHitPoint = _value;
|
||||
};
|
||||
} forEach _hitPointRows;
|
||||
|
||||
1 - ((_damage max _worstHitPoint) min 1)
|
||||
}],
|
||||
["buildStoredVehicle", compileFinal {
|
||||
params [["_plate", "", [""]], ["_vehicleData", createHashMap, [createHashMap]]];
|
||||
|
||||
private _className = _vehicleData getOrDefault ["classname", ""];
|
||||
private _damage = _vehicleData getOrDefault ["damage", 0];
|
||||
private _fuel = _vehicleData getOrDefault ["fuel", 0];
|
||||
private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
|
||||
private _hitPointRows = _self call ["buildHitPointRows", [_hitPoints]];
|
||||
|
||||
createHashMapFromArray [
|
||||
["entryKind", "stored"],
|
||||
["plate", _plate],
|
||||
["classname", _className],
|
||||
["displayName", _self call ["resolveDisplayName", [_className]]],
|
||||
["picture", _self call ["resolvePicture", [_className]]],
|
||||
["category", _self call ["resolveCategory", [_className]]],
|
||||
["damage", _damage],
|
||||
["fuel", _fuel],
|
||||
["health", _self call ["resolveHealth", [_damage, _hitPointRows]]],
|
||||
["hitPoints", _hitPointRows]
|
||||
]
|
||||
}],
|
||||
["buildNearbyVehicle", compileFinal {
|
||||
params [
|
||||
["_vehicle", objNull, [objNull]],
|
||||
["_origin", [], [[]]]
|
||||
];
|
||||
|
||||
if (isNull _vehicle) exitWith { createHashMap };
|
||||
|
||||
private _className = typeOf _vehicle;
|
||||
private _rawHitPoints = getAllHitPointsDamage _vehicle;
|
||||
private _hitPointRows = [];
|
||||
if (_rawHitPoints isEqualType [] && { count _rawHitPoints >= 3 }) then {
|
||||
private _names = _rawHitPoints param [0, []];
|
||||
private _selections = _rawHitPoints param [1, []];
|
||||
private _values = _rawHitPoints param [2, []];
|
||||
private _count = count _names;
|
||||
|
||||
for "_index" from 0 to (_count - 1) do {
|
||||
_hitPointRows pushBack (createHashMapFromArray [
|
||||
["name", _names param [_index, ""]],
|
||||
["selection", _selections param [_index, ""]],
|
||||
["value", _values param [_index, 0]]
|
||||
]);
|
||||
};
|
||||
};
|
||||
|
||||
private _damage = damage _vehicle;
|
||||
private _distance = if (_origin isEqualType [] && { count _origin >= 2 }) then {
|
||||
_vehicle distance2D _origin
|
||||
} else {
|
||||
_vehicle distance2D player
|
||||
};
|
||||
private _ownerUid = _vehicle getVariable ["forge_garage_owner_uid", ""];
|
||||
private _plate = _vehicle getVariable ["forge_garage_plate", ""];
|
||||
|
||||
createHashMapFromArray [
|
||||
["entryKind", "nearby"],
|
||||
["netId", netId _vehicle],
|
||||
["plate", _plate],
|
||||
["classname", _className],
|
||||
["displayName", _self call ["resolveDisplayName", [_className]]],
|
||||
["picture", _self call ["resolvePicture", [_className]]],
|
||||
["category", _self call ["resolveCategory", [_className]]],
|
||||
["damage", _damage],
|
||||
["fuel", fuel _vehicle],
|
||||
["health", _self call ["resolveHealth", [_damage, _hitPointRows]]],
|
||||
["hitPoints", _hitPointRows],
|
||||
["distance", _distance],
|
||||
["ownerUid", _ownerUid],
|
||||
["isEmpty", crew _vehicle isEqualTo []]
|
||||
]
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GarageCatalogService) = createHashMapObject [GVAR(GarageCatalogServiceBaseClass)];
|
||||
GVAR(GarageCatalogService)
|
||||
@ -1,7 +1,7 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initGarageClass.sqf
|
||||
* File: fnc_initClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-17
|
||||
* Last Update: 2026-02-13
|
||||
@ -18,7 +18,7 @@
|
||||
* Garage class object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_initGarageClass
|
||||
* call forge_client_garage_fnc_initClass
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
@ -48,8 +48,8 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [
|
||||
["sync", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _garage = _self get "garage";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
private _garage = createHashMap;
|
||||
|
||||
{ _garage set [_x, _y]; } forEach _data;
|
||||
_self set ["garage", _garage];
|
||||
@ -57,6 +57,9 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Garage] Sync completed";
|
||||
}],
|
||||
["getGarageState", compileFinal {
|
||||
_self getOrDefault ["garage", createHashMap]
|
||||
}],
|
||||
["get", compileFinal {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
298
arma/client/addons/garage/functions/fnc_initSessionService.sqf
Normal file
298
arma/client/addons/garage/functions/fnc_initSessionService.sqf
Normal file
@ -0,0 +1,298 @@
|
||||
#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)
|
||||
205
arma/client/addons/garage/functions/fnc_initUIBridge.sqf
Normal file
205
arma/client/addons/garage/functions/fnc_initUIBridge.sqf
Normal file
@ -0,0 +1,205 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-14
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the garage UI bridge for browser control state and retrieve/store actions.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
private _webUIDeclarations = call EFUNC(common,initWebUIBridge);
|
||||
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 {
|
||||
_self call ["setActiveBrowserControl", [controlNull]];
|
||||
controlNull
|
||||
};
|
||||
|
||||
private _control = _display displayCtrl 1006;
|
||||
_self call ["setActiveBrowserControl", [_control]];
|
||||
_control
|
||||
}],
|
||||
["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", ["garage::hydrate", GVAR(GarageSessionService) 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", []];
|
||||
};
|
||||
};
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(GarageUIBridge) = createHashMapObject [GVAR(GarageUIBridgeBaseClass)];
|
||||
GVAR(GarageUIBridge)
|
||||
@ -19,3 +19,20 @@
|
||||
* Example:
|
||||
* call forge_client_garage_fnc_openUI;
|
||||
*/
|
||||
|
||||
private _display = createDialog ["RscGarage", true];
|
||||
private _ctrl = _display displayCtrl 1006;
|
||||
|
||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
||||
}];
|
||||
|
||||
if !(isNil QGVAR(GarageUIBridge)) then {
|
||||
GVAR(GarageUIBridge) call ["setActiveBrowserControl", [_ctrl]];
|
||||
};
|
||||
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
|
||||
|
||||
true;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
class RscGarage {
|
||||
idd = 1000;
|
||||
idd = 1005;
|
||||
fadeIn = 0;
|
||||
fadeOut = 0;
|
||||
duration = 1e011;
|
||||
@ -10,7 +10,7 @@ class RscGarage {
|
||||
class controls {
|
||||
class IFrame: RscText {
|
||||
type = 106;
|
||||
idc = 1001;
|
||||
idc = 1006;
|
||||
x = "safeZoneXAbs";
|
||||
y = "safeZoneY";
|
||||
w = "safeZoneWAbs";
|
||||
|
||||
574
arma/client/addons/garage/ui/_site/garage-ui.css
Normal file
574
arma/client/addons/garage/ui/_site/garage-ui.css
Normal file
@ -0,0 +1,574 @@
|
||||
/* Generated by tools/build-webui.mjs for Garage UI styles. Do not edit directly. */
|
||||
:root {
|
||||
--garage-shell-bg: #e4e3df;
|
||||
--garage-surface: #f5f3ef;
|
||||
--garage-surface-alt: #ece8e2;
|
||||
--garage-border: rgba(74, 91, 110, 0.2);
|
||||
--garage-border-strong: rgba(20, 46, 79, 0.18);
|
||||
--garage-text-main: #1f2d3d;
|
||||
--garage-text-muted: #6a7787;
|
||||
--garage-text-subtle: #8792a0;
|
||||
--garage-accent: #12365d;
|
||||
--garage-accent-soft: #dbe7f3;
|
||||
--garage-accent-line: rgba(18, 54, 93, 0.12);
|
||||
--garage-warning: #8f5f26;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", "Trebuchet MS", sans-serif;
|
||||
color: var(--garage-text-main);
|
||||
background: var(--garage-shell-bg);
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid rgb(18 54 93 / 0.35);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.garage-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--garage-shell-bg);
|
||||
}
|
||||
|
||||
.garage-layout {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: min(100%, 1613px);
|
||||
margin: 0 auto;
|
||||
padding: 1.25rem;
|
||||
display: grid;
|
||||
grid-template-columns: 308px minmax(0, 1fr);
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.garage-sidebar,
|
||||
.garage-main {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.garage-main {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.garage-module,
|
||||
.garage-panel,
|
||||
.garage-card {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--garage-surface) 0%,
|
||||
var(--garage-surface-alt) 100%
|
||||
);
|
||||
border: 1px solid var(--garage-border);
|
||||
border-radius: 1.35rem;
|
||||
}
|
||||
|
||||
.garage-module,
|
||||
.garage-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.garage-module {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.garage-panel {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.garage-panel-header,
|
||||
.garage-module-header,
|
||||
.garage-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.garage-panel-header {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.garage-module-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.garage-panel-intro {
|
||||
padding: 0 1rem 1rem;
|
||||
border-bottom: 1px solid var(--garage-accent-line);
|
||||
}
|
||||
|
||||
.garage-dashboard {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.garage-list-card,
|
||||
.garage-detail-card {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.garage-detail-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.garage-scroll-body {
|
||||
flex: 1;
|
||||
min-height: 20rem;
|
||||
max-height: 24rem;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
padding-right: 0.2rem;
|
||||
}
|
||||
|
||||
.garage-detail-body {
|
||||
padding-top: 0.95rem;
|
||||
}
|
||||
|
||||
.garage-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.85fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.garage-detail-meta,
|
||||
.garage-summary-grid,
|
||||
.garage-search-actions,
|
||||
.garage-category-grid,
|
||||
.garage-action-row,
|
||||
.garage-inline-meters,
|
||||
.garage-hitpoint-grid,
|
||||
.garage-footer {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.garage-detail-meta {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.garage-summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.garage-summary-grid > :last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.garage-search-actions,
|
||||
.garage-action-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.garage-category-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.garage-footer {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
padding: 0.95rem 1.25rem 1.15rem;
|
||||
border-top: 1px solid rgb(18 54 93 / 0.1);
|
||||
}
|
||||
|
||||
.garage-meter-stack {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.garage-eyebrow,
|
||||
.garage-footer-title,
|
||||
.garage-stat-label,
|
||||
.garage-meter-label,
|
||||
.garage-hitpoint-selection {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--garage-text-subtle);
|
||||
}
|
||||
|
||||
.garage-title,
|
||||
.garage-section-title {
|
||||
margin: 0.16rem 0 0;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.garage-section-title {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.garage-copy,
|
||||
.garage-detail-note,
|
||||
.garage-empty-copy,
|
||||
.garage-footer-copy,
|
||||
.garage-vehicle-meta,
|
||||
.garage-detail-caption {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.48;
|
||||
color: var(--garage-text-muted);
|
||||
}
|
||||
|
||||
.garage-pill,
|
||||
.garage-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.48rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
background: var(--garage-accent-soft);
|
||||
color: var(--garage-accent);
|
||||
}
|
||||
|
||||
.garage-badge.is-warning {
|
||||
background: rgb(246 226 193 / 0.88);
|
||||
color: var(--garage-warning);
|
||||
}
|
||||
|
||||
.garage-search-form {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.garage-search-input {
|
||||
width: 100%;
|
||||
height: 2.9rem;
|
||||
padding: 0 0.95rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.75);
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-stat-card {
|
||||
min-width: 0;
|
||||
padding: 0.85rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.48);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.garage-stat-card.is-accent {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(237 243 249 / 0.92) 0%,
|
||||
rgb(223 232 242 / 0.72) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.garage-stat-card.is-danger {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(254 242 242 / 0.95) 0%,
|
||||
rgb(252 225 225 / 0.82) 100%
|
||||
);
|
||||
border-color: rgb(220 151 151 / 0.38);
|
||||
}
|
||||
|
||||
.garage-stat-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
line-height: 1.3;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.garage-chip {
|
||||
min-height: 2.6rem;
|
||||
padding: 0.68rem 0.9rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.52);
|
||||
color: var(--garage-text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.garage-chip.is-active {
|
||||
background: var(--garage-accent-soft);
|
||||
color: var(--garage-accent);
|
||||
border-color: rgb(18 54 93 / 0.2);
|
||||
}
|
||||
|
||||
.garage-vehicle-item {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 0.95rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.48);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.garage-vehicle-item.is-selected {
|
||||
border-color: rgb(18 54 93 / 0.24);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(237 243 249 / 0.96) 0%,
|
||||
rgb(223 232 242 / 0.74) 100%
|
||||
);
|
||||
box-shadow: 0 16px 26px rgb(18 54 93 / 0.08);
|
||||
}
|
||||
|
||||
.garage-vehicle-item-head,
|
||||
.garage-meter-label-row,
|
||||
.garage-subsystem-header,
|
||||
.garage-hitpoint-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.garage-vehicle-copy,
|
||||
.garage-hitpoint-copy,
|
||||
.garage-footer-block {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.garage-vehicle-title,
|
||||
.garage-hitpoint-name,
|
||||
.garage-hitpoint-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-meter {
|
||||
display: grid;
|
||||
gap: 0.32rem;
|
||||
}
|
||||
|
||||
.garage-meter-track {
|
||||
width: 100%;
|
||||
height: 0.45rem;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgb(18 54 93 / 0.08);
|
||||
}
|
||||
|
||||
.garage-meter-value {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-meter-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.garage-meter-fill.is-health {
|
||||
background: linear-gradient(90deg, #2f7d5b 0%, #4eaa82 100%);
|
||||
}
|
||||
|
||||
.garage-meter-fill.is-fuel {
|
||||
background: linear-gradient(90deg, #12365d 0%, #3c6792 100%);
|
||||
}
|
||||
|
||||
.garage-btn {
|
||||
min-height: 2.75rem;
|
||||
padding: 0.72rem 1rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--garage-border-strong);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.garage-btn-primary {
|
||||
background: rgb(255 255 255 / 0.68);
|
||||
color: var(--garage-accent);
|
||||
}
|
||||
|
||||
.garage-btn-primary:hover {
|
||||
background: rgb(219 231 243 / 0.88);
|
||||
}
|
||||
|
||||
.garage-btn-secondary {
|
||||
background: rgb(255 255 255 / 0.42);
|
||||
color: var(--garage-text-muted);
|
||||
}
|
||||
|
||||
.garage-btn-secondary:hover {
|
||||
background: rgb(255 255 255 / 0.6);
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-hitpoint-row {
|
||||
padding: 0.72rem 0.78rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.52);
|
||||
}
|
||||
|
||||
.garage-detail-empty,
|
||||
.garage-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.garage-empty-title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-empty-inline {
|
||||
padding: 0.9rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px dashed var(--garage-border);
|
||||
color: var(--garage-text-muted);
|
||||
background: rgb(255 255 255 / 0.36);
|
||||
}
|
||||
|
||||
.garage-toast-stack {
|
||||
position: fixed;
|
||||
top: 1.2rem;
|
||||
right: 1.5rem;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.garage-toast {
|
||||
max-width: 24rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 28px rgb(16 34 56 / 0.14);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.garage-toast.is-success {
|
||||
background: #ecfdf5;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.garage-toast.is-error {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.garage-layout {
|
||||
grid-template-columns: 288px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.garage-detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.garage-layout {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.garage-main,
|
||||
.garage-sidebar {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.garage-dashboard {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.garage-detail-card {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.garage-scroll-body {
|
||||
max-height: none;
|
||||
min-height: 16rem;
|
||||
}
|
||||
|
||||
.garage-footer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
1291
arma/client/addons/garage/ui/_site/garage-ui.js
Normal file
1291
arma/client/addons/garage/ui/_site/garage-ui.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,261 +1,64 @@
|
||||
<!-- Generated by tools/build-webui.mjs for garage UI index. Do not edit directly. -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vehicle Garage</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
This approach is used instead of static HTML imports to work with Arma 3's file system
|
||||
-->
|
||||
<title>FORGE Vehicle Garage</title>
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\garage\\ui\\_site\\style.css",
|
||||
),
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\garage\\ui\\_site\\script.js",
|
||||
),
|
||||
]).then(([css, js]) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
window.ForgeSiteConfig = {
|
||||
addonName: "garage",
|
||||
logLabel: "Garage UI",
|
||||
styles: ["garage-ui.css"],
|
||||
commonScripts: ["forge-webui.js"],
|
||||
scripts: ["garage-ui.js"],
|
||||
};
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
(function loadForgeSiteLoader() {
|
||||
const armaLoaderPath =
|
||||
"forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js";
|
||||
const browserLoaderPath =
|
||||
"../../../common/ui/_site/forge-site-loader.js";
|
||||
|
||||
function appendScript(js) {
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function requestLoader() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
A3API &&
|
||||
typeof A3API.RequestFile === "function"
|
||||
) {
|
||||
return A3API.RequestFile(armaLoaderPath);
|
||||
}
|
||||
|
||||
return fetch(browserLoaderPath).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"Failed to load " + browserLoaderPath,
|
||||
);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
requestLoader()
|
||||
.then(appendScript)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[Garage UI] Failed to load Forge site loader.",
|
||||
error,
|
||||
);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="garage-container">
|
||||
<!-- Header Section -->
|
||||
<div class="garage-header">
|
||||
<div class="garage-logo">
|
||||
<div class="logo-icon">🚗</div>
|
||||
</div>
|
||||
<div class="garage-info">
|
||||
<h1 class="garage-title">Vehicle Garage</h1>
|
||||
<p class="garage-subtitle">Vehicle Management System</p>
|
||||
</div>
|
||||
<div class="garage-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Stored</span>
|
||||
<span class="stat-value" id="storedCount">12</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Active</span>
|
||||
<span class="stat-value" id="activeCount">2</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Capacity</span>
|
||||
<span class="stat-value" id="capacityCount">20</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="action-btn close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="garage-content">
|
||||
<!-- Left Panel - Filters -->
|
||||
<div class="garage-panel filters-panel">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">Filters</h2>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<!-- Status Filter -->
|
||||
<div class="filter-section">
|
||||
<h3 class="filter-title">Status</h3>
|
||||
<div class="filter-buttons">
|
||||
<button
|
||||
class="filter-btn active"
|
||||
data-filter="all"
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="stored">
|
||||
Stored
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="active">
|
||||
Active
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Type Filter -->
|
||||
<div class="filter-section">
|
||||
<h3 class="filter-title">Vehicle Type</h3>
|
||||
<div class="type-list">
|
||||
<button
|
||||
class="type-item active"
|
||||
data-type="all"
|
||||
>
|
||||
<span class="type-icon">📦</span>
|
||||
<span class="type-name">All Types</span>
|
||||
</button>
|
||||
<button class="type-item" data-type="car">
|
||||
<span class="type-icon">🚗</span>
|
||||
<span class="type-name">Cars</span>
|
||||
</button>
|
||||
<button class="type-item" data-type="truck">
|
||||
<span class="type-icon">🚛</span>
|
||||
<span class="type-name">Trucks</span>
|
||||
</button>
|
||||
<button class="type-item" data-type="air">
|
||||
<span class="type-icon">🚁</span>
|
||||
<span class="type-name">Aircraft</span>
|
||||
</button>
|
||||
<button class="type-item" data-type="sea">
|
||||
<span class="type-icon">🚤</span>
|
||||
<span class="type-name">Boats</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="filter-section">
|
||||
<h3 class="filter-title">Search</h3>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
id="searchInput"
|
||||
placeholder="Search vehicles..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Panel - Vehicle Grid -->
|
||||
<div class="garage-panel vehicles-panel">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">Your Vehicles</h2>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="vehicles-grid" id="vehiclesGrid">
|
||||
<!-- Vehicles will be dynamically generated -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel - Vehicle Details -->
|
||||
<div class="garage-panel details-panel" id="detailsPanel">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">Vehicle Details</h2>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="no-selection" id="noSelection">
|
||||
<div class="no-selection-icon">🚗</div>
|
||||
<p>Select a vehicle to view details</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="vehicle-details"
|
||||
id="vehicleDetails"
|
||||
style="display: none"
|
||||
>
|
||||
<div class="detail-header">
|
||||
<div class="detail-icon" id="detailIcon">
|
||||
🚗
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<h3 class="detail-name" id="detailName">
|
||||
Vehicle Name
|
||||
</h3>
|
||||
<p class="detail-type" id="detailType">
|
||||
Type
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-stats">
|
||||
<div class="detail-stat">
|
||||
<span class="detail-label">Status</span>
|
||||
<span class="detail-value" id="detailStatus"
|
||||
>Stored</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-stat">
|
||||
<span class="detail-label">Condition</span>
|
||||
<span
|
||||
class="detail-value"
|
||||
id="detailCondition"
|
||||
>100%</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-stat">
|
||||
<span class="detail-label">Fuel</span>
|
||||
<span class="detail-value" id="detailFuel"
|
||||
>100%</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-stat">
|
||||
<span class="detail-label">Location</span>
|
||||
<span
|
||||
class="detail-value"
|
||||
id="detailLocation"
|
||||
>Garage A</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-actions">
|
||||
<button
|
||||
class="detail-btn spawn-btn"
|
||||
id="spawnBtn"
|
||||
>
|
||||
<span class="btn-icon">🚀</span>
|
||||
<span class="btn-text">Spawn Vehicle</span>
|
||||
</button>
|
||||
<button
|
||||
class="detail-btn store-btn"
|
||||
id="storeBtn"
|
||||
style="display: none"
|
||||
>
|
||||
<span class="btn-icon">📦</span>
|
||||
<span class="btn-text">Store Vehicle</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="detail-specs">
|
||||
<h4 class="specs-title">Specifications</h4>
|
||||
<div class="specs-list">
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">Seats</span>
|
||||
<span
|
||||
class="spec-value"
|
||||
id="detailSeats"
|
||||
>4</span
|
||||
>
|
||||
</div>
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">Speed</span>
|
||||
<span
|
||||
class="spec-value"
|
||||
id="detailSpeed"
|
||||
>180 km/h</span
|
||||
>
|
||||
</div>
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">Cargo</span>
|
||||
<span
|
||||
class="spec-value"
|
||||
id="detailCargo"
|
||||
>200 kg</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,472 +0,0 @@
|
||||
/**
|
||||
* Vehicle Garage Interface
|
||||
* Handles vehicle management with spawn and store actions
|
||||
*/
|
||||
|
||||
// Mock data - sample vehicles
|
||||
const mockData = {
|
||||
vehicles: [
|
||||
// Cars
|
||||
{
|
||||
id: 1,
|
||||
name: "Sedan",
|
||||
type: "car",
|
||||
icon: "🚗",
|
||||
status: "stored",
|
||||
condition: 95,
|
||||
fuel: 80,
|
||||
location: "Garage A",
|
||||
seats: 4,
|
||||
speed: "180 km/h",
|
||||
cargo: "200 kg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Sports Car",
|
||||
type: "car",
|
||||
icon: "🏎️",
|
||||
status: "stored",
|
||||
condition: 100,
|
||||
fuel: 100,
|
||||
location: "Garage A",
|
||||
seats: 2,
|
||||
speed: "250 km/h",
|
||||
cargo: "50 kg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "SUV",
|
||||
type: "car",
|
||||
icon: "🚙",
|
||||
status: "active",
|
||||
condition: 85,
|
||||
fuel: 60,
|
||||
location: "In Use",
|
||||
seats: 6,
|
||||
speed: "160 km/h",
|
||||
cargo: "400 kg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Hatchback",
|
||||
type: "car",
|
||||
icon: "🚗",
|
||||
status: "stored",
|
||||
condition: 90,
|
||||
fuel: 75,
|
||||
location: "Garage B",
|
||||
seats: 4,
|
||||
speed: "170 km/h",
|
||||
cargo: "250 kg",
|
||||
},
|
||||
|
||||
// Trucks
|
||||
{
|
||||
id: 5,
|
||||
name: "Pickup Truck",
|
||||
type: "truck",
|
||||
icon: "🚛",
|
||||
status: "stored",
|
||||
condition: 88,
|
||||
fuel: 70,
|
||||
location: "Garage A",
|
||||
seats: 2,
|
||||
speed: "140 km/h",
|
||||
cargo: "800 kg",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Delivery Van",
|
||||
type: "truck",
|
||||
icon: "🚚",
|
||||
status: "stored",
|
||||
condition: 92,
|
||||
fuel: 85,
|
||||
location: "Garage B",
|
||||
seats: 3,
|
||||
speed: "130 km/h",
|
||||
cargo: "1200 kg",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "Heavy Truck",
|
||||
type: "truck",
|
||||
icon: "🚛",
|
||||
status: "active",
|
||||
condition: 75,
|
||||
fuel: 50,
|
||||
location: "In Use",
|
||||
seats: 2,
|
||||
speed: "120 km/h",
|
||||
cargo: "2000 kg",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Box Truck",
|
||||
type: "truck",
|
||||
icon: "📦",
|
||||
status: "stored",
|
||||
condition: 80,
|
||||
fuel: 65,
|
||||
location: "Garage A",
|
||||
seats: 3,
|
||||
speed: "110 km/h",
|
||||
cargo: "1500 kg",
|
||||
},
|
||||
|
||||
// Aircraft
|
||||
{
|
||||
id: 9,
|
||||
name: "Helicopter",
|
||||
type: "air",
|
||||
icon: "🚁",
|
||||
status: "stored",
|
||||
condition: 95,
|
||||
fuel: 90,
|
||||
location: "Helipad",
|
||||
seats: 6,
|
||||
speed: "280 km/h",
|
||||
cargo: "500 kg",
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Light Plane",
|
||||
type: "air",
|
||||
icon: "✈️",
|
||||
status: "stored",
|
||||
condition: 100,
|
||||
fuel: 100,
|
||||
location: "Hangar",
|
||||
seats: 4,
|
||||
speed: "320 km/h",
|
||||
cargo: "300 kg",
|
||||
},
|
||||
|
||||
// Boats
|
||||
{
|
||||
id: 11,
|
||||
name: "Speedboat",
|
||||
type: "sea",
|
||||
icon: "🚤",
|
||||
status: "stored",
|
||||
condition: 93,
|
||||
fuel: 80,
|
||||
location: "Marina",
|
||||
seats: 4,
|
||||
speed: "100 km/h",
|
||||
cargo: "150 kg",
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: "Yacht",
|
||||
type: "sea",
|
||||
icon: "🛥️",
|
||||
status: "stored",
|
||||
condition: 98,
|
||||
fuel: 95,
|
||||
location: "Marina",
|
||||
seats: 12,
|
||||
speed: "60 km/h",
|
||||
cargo: "800 kg",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// State
|
||||
let selectedVehicle = null;
|
||||
let statusFilter = "all";
|
||||
let typeFilter = "all";
|
||||
let searchQuery = "";
|
||||
|
||||
// Icons by type
|
||||
const typeIcons = {
|
||||
car: "🚗",
|
||||
truck: "🚛",
|
||||
air: "🚁",
|
||||
sea: "🚤",
|
||||
};
|
||||
|
||||
// Initialize
|
||||
function initGarage() {
|
||||
console.log("Garage interface initializing...");
|
||||
|
||||
setupEventHandlers();
|
||||
renderVehicles();
|
||||
updateStats();
|
||||
|
||||
console.log("Garage interface initialized");
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
function setupEventHandlers() {
|
||||
// Close button
|
||||
const closeBtn = document.querySelector(".close-btn");
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener("click", () => {
|
||||
console.log("Closing garage...");
|
||||
sendEvent("garage::close", {});
|
||||
});
|
||||
}
|
||||
|
||||
// Status filters
|
||||
const filterBtns = document.querySelectorAll(".filter-btn");
|
||||
filterBtns.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
filterBtns.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
statusFilter = btn.dataset.filter;
|
||||
renderVehicles();
|
||||
});
|
||||
});
|
||||
|
||||
// Type filters
|
||||
const typeItems = document.querySelectorAll(".type-item");
|
||||
typeItems.forEach((item) => {
|
||||
item.addEventListener("click", () => {
|
||||
typeItems.forEach((i) => i.classList.remove("active"));
|
||||
item.classList.add("active");
|
||||
typeFilter = item.dataset.type;
|
||||
renderVehicles();
|
||||
});
|
||||
});
|
||||
|
||||
// Search
|
||||
const searchInput = document.getElementById("searchInput");
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener("input", (e) => {
|
||||
searchQuery = e.target.value.toLowerCase();
|
||||
renderVehicles();
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn button
|
||||
const spawnBtn = document.getElementById("spawnBtn");
|
||||
if (spawnBtn) {
|
||||
spawnBtn.addEventListener("click", () => {
|
||||
if (selectedVehicle) {
|
||||
spawnVehicle(selectedVehicle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Store button
|
||||
const storeBtn = document.getElementById("storeBtn");
|
||||
if (storeBtn) {
|
||||
storeBtn.addEventListener("click", () => {
|
||||
if (selectedVehicle) {
|
||||
storeVehicle(selectedVehicle);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Render vehicles
|
||||
function renderVehicles() {
|
||||
const vehiclesGrid = document.getElementById("vehiclesGrid");
|
||||
if (!vehiclesGrid) return;
|
||||
|
||||
vehiclesGrid.innerHTML = "";
|
||||
|
||||
// Filter vehicles
|
||||
let filtered = mockData.vehicles;
|
||||
|
||||
// Status filter
|
||||
if (statusFilter !== "all") {
|
||||
filtered = filtered.filter((v) => v.status === statusFilter);
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (typeFilter !== "all") {
|
||||
filtered = filtered.filter((v) => v.type === typeFilter);
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (searchQuery) {
|
||||
filtered = filtered.filter(
|
||||
(v) =>
|
||||
v.name.toLowerCase().includes(searchQuery) ||
|
||||
v.type.toLowerCase().includes(searchQuery),
|
||||
);
|
||||
}
|
||||
|
||||
// Render vehicles
|
||||
filtered.forEach((vehicle) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "vehicle-card";
|
||||
if (selectedVehicle && selectedVehicle.id === vehicle.id) {
|
||||
card.classList.add("selected");
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="vehicle-icon">${vehicle.icon}</div>
|
||||
<div class="vehicle-name">${vehicle.name}</div>
|
||||
<div class="vehicle-type">${vehicle.type}</div>
|
||||
<div class="vehicle-status ${vehicle.status}">${vehicle.status}</div>
|
||||
`;
|
||||
|
||||
card.addEventListener("click", () => selectVehicle(vehicle));
|
||||
vehiclesGrid.appendChild(card);
|
||||
});
|
||||
|
||||
console.log(`Rendered ${filtered.length} vehicles`);
|
||||
}
|
||||
|
||||
// Select vehicle
|
||||
function selectVehicle(vehicle) {
|
||||
selectedVehicle = vehicle;
|
||||
|
||||
// Update selected state in grid
|
||||
document.querySelectorAll(".vehicle-card").forEach((card) => {
|
||||
card.classList.remove("selected");
|
||||
});
|
||||
event.currentTarget.classList.add("selected");
|
||||
|
||||
// Show details
|
||||
showVehicleDetails(vehicle);
|
||||
}
|
||||
|
||||
// Show vehicle details
|
||||
function showVehicleDetails(vehicle) {
|
||||
const noSelection = document.getElementById("noSelection");
|
||||
const vehicleDetails = document.getElementById("vehicleDetails");
|
||||
const spawnBtn = document.getElementById("spawnBtn");
|
||||
const storeBtn = document.getElementById("storeBtn");
|
||||
|
||||
if (noSelection) noSelection.style.display = "none";
|
||||
if (vehicleDetails) vehicleDetails.style.display = "flex";
|
||||
|
||||
// Update details
|
||||
document.getElementById("detailIcon").textContent = vehicle.icon;
|
||||
document.getElementById("detailName").textContent = vehicle.name;
|
||||
document.getElementById("detailType").textContent = vehicle.type;
|
||||
document.getElementById("detailStatus").textContent = vehicle.status;
|
||||
document.getElementById("detailCondition").textContent =
|
||||
`${vehicle.condition}%`;
|
||||
document.getElementById("detailFuel").textContent = `${vehicle.fuel}%`;
|
||||
document.getElementById("detailLocation").textContent = vehicle.location;
|
||||
document.getElementById("detailSeats").textContent = vehicle.seats;
|
||||
document.getElementById("detailSpeed").textContent = vehicle.speed;
|
||||
document.getElementById("detailCargo").textContent = vehicle.cargo;
|
||||
|
||||
// Show/hide action buttons based on status
|
||||
if (vehicle.status === "stored") {
|
||||
spawnBtn.style.display = "flex";
|
||||
storeBtn.style.display = "none";
|
||||
} else {
|
||||
spawnBtn.style.display = "none";
|
||||
storeBtn.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn vehicle
|
||||
function spawnVehicle(vehicle) {
|
||||
console.log("Spawning vehicle:", vehicle.name);
|
||||
|
||||
// Update local state
|
||||
vehicle.status = "active";
|
||||
vehicle.location = "In Use";
|
||||
|
||||
sendEvent("garage::spawn", {
|
||||
vehicleId: vehicle.id,
|
||||
vehicleName: vehicle.name,
|
||||
vehicleType: vehicle.type,
|
||||
});
|
||||
|
||||
// Re-render
|
||||
renderVehicles();
|
||||
updateStats();
|
||||
if (selectedVehicle && selectedVehicle.id === vehicle.id) {
|
||||
showVehicleDetails(vehicle);
|
||||
}
|
||||
}
|
||||
|
||||
// Store vehicle
|
||||
function storeVehicle(vehicle) {
|
||||
console.log("Storing vehicle:", vehicle.name);
|
||||
|
||||
// Update local state
|
||||
vehicle.status = "stored";
|
||||
vehicle.location = "Garage A";
|
||||
|
||||
sendEvent("garage::store", {
|
||||
vehicleId: vehicle.id,
|
||||
vehicleName: vehicle.name,
|
||||
vehicleType: vehicle.type,
|
||||
});
|
||||
|
||||
// Re-render
|
||||
renderVehicles();
|
||||
updateStats();
|
||||
if (selectedVehicle && selectedVehicle.id === vehicle.id) {
|
||||
showVehicleDetails(vehicle);
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats
|
||||
function updateStats() {
|
||||
const stored = mockData.vehicles.filter(
|
||||
(v) => v.status === "stored",
|
||||
).length;
|
||||
const active = mockData.vehicles.filter(
|
||||
(v) => v.status === "active",
|
||||
).length;
|
||||
const capacity = mockData.vehicles.length + 6; // Mock capacity
|
||||
|
||||
document.getElementById("storedCount").textContent = stored;
|
||||
document.getElementById("activeCount").textContent = active;
|
||||
document.getElementById("capacityCount").textContent = capacity;
|
||||
}
|
||||
|
||||
// Update garage data from external source
|
||||
function updateGarageData(data) {
|
||||
if (data.vehicles) {
|
||||
mockData.vehicles = data.vehicles;
|
||||
renderVehicles();
|
||||
updateStats();
|
||||
|
||||
// Update selected vehicle if it still exists
|
||||
if (selectedVehicle) {
|
||||
const updated = mockData.vehicles.find(
|
||||
(v) => v.id === selectedVehicle.id,
|
||||
);
|
||||
if (updated) {
|
||||
selectedVehicle = updated;
|
||||
showVehicleDetails(updated);
|
||||
} else {
|
||||
selectedVehicle = null;
|
||||
const noSelection = document.getElementById("noSelection");
|
||||
const vehicleDetails =
|
||||
document.getElementById("vehicleDetails");
|
||||
if (noSelection) noSelection.style.display = "flex";
|
||||
if (vehicleDetails) vehicleDetails.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send event to Arma
|
||||
function sendEvent(event, data) {
|
||||
if (typeof A3API !== "undefined") {
|
||||
A3API.SendAlert(
|
||||
JSON.stringify({
|
||||
event: event,
|
||||
data: data,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.log("Event:", event, "Data:", data);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initGarage);
|
||||
} else {
|
||||
initGarage();
|
||||
}
|
||||
|
||||
// Expose functions globally
|
||||
window.updateGarageData = updateGarageData;
|
||||
window.selectVehicle = selectVehicle;
|
||||
window.spawnVehicle = spawnVehicle;
|
||||
window.storeVehicle = storeVehicle;
|
||||
@ -1,605 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
font-family: Arial, sans-serif;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.garage-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Header Section */
|
||||
.garage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: rgba(15, 20, 30, 0.9);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 150, 200, 0.15),
|
||||
0 4px 16px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.garage-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: rgba(20, 30, 45, 0.8);
|
||||
border: 2px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.garage-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.garage-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.garage-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.garage-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.2),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border-color: rgba(200, 100, 100, 0.4);
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
border-color: rgba(255, 100, 100, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(200, 100, 100, 0.2),
|
||||
inset 0 0 20px rgba(200, 100, 100, 0.05);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.garage-content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr 350px;
|
||||
gap: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.garage-panel {
|
||||
background: rgba(15, 20, 30, 0.9);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 150, 200, 0.1),
|
||||
0 4px 16px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.panel-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.panel-content::-webkit-scrollbar-track {
|
||||
background: rgba(15, 20, 30, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 150, 200, 0.5);
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
.filter-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.filter-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(160, 180, 200, 0.85);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
flex: 1;
|
||||
padding: 0.625rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.8);
|
||||
border-color: rgba(150, 200, 255, 0.5);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.6);
|
||||
box-shadow:
|
||||
0 0 10px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 15px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.type-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.type-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.type-item:hover {
|
||||
background: rgba(30, 45, 70, 0.8);
|
||||
border-left-color: rgba(150, 200, 255, 0.7);
|
||||
}
|
||||
|
||||
.type-item.active {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-left-color: rgba(100, 200, 150, 0.8);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 200, 150, 0.15),
|
||||
inset 0 0 20px rgba(100, 200, 150, 0.05);
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(150, 200, 255, 0.6);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: rgba(100, 120, 140, 0.6);
|
||||
}
|
||||
|
||||
/* Vehicles Grid */
|
||||
.vehicles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.vehicle-card {
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
padding: 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.vehicle-card:hover {
|
||||
background: rgba(30, 45, 70, 0.7);
|
||||
border-left-color: rgba(150, 200, 255, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.vehicle-card.selected {
|
||||
background: rgba(30, 45, 70, 0.8);
|
||||
border-left-color: rgba(100, 200, 150, 0.8);
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 200, 150, 0.2),
|
||||
inset 0 0 25px rgba(100, 200, 150, 0.05);
|
||||
}
|
||||
|
||||
.vehicle-icon {
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vehicle-name {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vehicle-type {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.vehicle-status {
|
||||
padding: 0.375rem;
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vehicle-status.stored {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.4);
|
||||
color: rgba(150, 200, 255, 0.9);
|
||||
}
|
||||
|
||||
.vehicle-status.active {
|
||||
background: rgba(100, 200, 150, 0.2);
|
||||
border-color: rgba(100, 200, 150, 0.4);
|
||||
color: rgba(150, 255, 200, 0.9);
|
||||
}
|
||||
|
||||
/* Vehicle Details */
|
||||
.no-selection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
.no-selection-icon {
|
||||
font-size: 4rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.no-selection p {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(140, 160, 180, 0.7);
|
||||
}
|
||||
|
||||
.vehicle-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-type {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.detail-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-stat {
|
||||
padding: 0.875rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.7);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.detail-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.9);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.2),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.spawn-btn {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.5);
|
||||
}
|
||||
|
||||
.spawn-btn:hover {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
}
|
||||
|
||||
.store-btn {
|
||||
background: rgba(200, 150, 100, 0.2);
|
||||
border-color: rgba(200, 150, 100, 0.4);
|
||||
}
|
||||
|
||||
.store-btn:hover {
|
||||
background: rgba(200, 150, 100, 0.3);
|
||||
border-color: rgba(255, 200, 150, 0.6);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
}
|
||||
|
||||
.detail-specs {
|
||||
padding: 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.5);
|
||||
border: 1px solid rgba(100, 150, 200, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.specs-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(180, 200, 220, 0.9);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.specs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(100, 150, 200, 0.15);
|
||||
}
|
||||
|
||||
.spec-item:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.spec-label {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(140, 160, 180, 0.85);
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1400px) {
|
||||
.garage-content {
|
||||
grid-template-columns: 220px 1fr 320px;
|
||||
}
|
||||
|
||||
.vehicles-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.garage-content {
|
||||
grid-template-columns: 1fr 350px;
|
||||
}
|
||||
|
||||
.filters-panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
19
arma/client/addons/garage/ui/src/bootstrap.js
vendored
Normal file
19
arma/client/addons/garage/ui/src/bootstrap.js
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
(function () {
|
||||
const ForgeWebUI = window.ForgeWebUI;
|
||||
const GarageApp = window.GarageApp;
|
||||
const app = ForgeWebUI.createApp({
|
||||
name: "garage",
|
||||
root: "#app",
|
||||
setup({ root }) {
|
||||
ForgeWebUI.mount(root, () => GarageApp.components.App(), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
|
||||
if (GarageApp.bridge) {
|
||||
GarageApp.bridge.notifyReady();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
app.start();
|
||||
})();
|
||||
87
arma/client/addons/garage/ui/src/bridge.js
Normal file
87
arma/client/addons/garage/ui/src/bridge.js
Normal file
@ -0,0 +1,87 @@
|
||||
(function () {
|
||||
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
||||
const store = GarageApp.store;
|
||||
const bridge = window.ForgeWebUI.createBridge({
|
||||
closeEvent: "garage::close",
|
||||
globalName: "ForgeBridge",
|
||||
readyEvent: "garage::ready",
|
||||
});
|
||||
|
||||
function requestClose() {
|
||||
return bridge.close({});
|
||||
}
|
||||
|
||||
function requestRefresh() {
|
||||
return bridge.send("garage::refresh", {});
|
||||
}
|
||||
|
||||
function requestRetrieve(payload) {
|
||||
return bridge.send("garage::vehicle::retrieve::request", payload);
|
||||
}
|
||||
|
||||
function requestStore(payload) {
|
||||
return bridge.send("garage::vehicle::store::request", payload);
|
||||
}
|
||||
|
||||
function notifyReady() {
|
||||
return bridge.ready({ loaded: true });
|
||||
}
|
||||
|
||||
function hydrate(payloadData) {
|
||||
GarageApp.data.applyHydratePayload(payloadData);
|
||||
store.hydrateFromPayload(payloadData);
|
||||
}
|
||||
|
||||
bridge.on("garage::hydrate", hydrate);
|
||||
bridge.on("garage::sync", hydrate);
|
||||
|
||||
bridge.on("garage::retrieve::success", (payloadData) => {
|
||||
store.finishAction();
|
||||
if (GarageApp.actions) {
|
||||
GarageApp.actions.showNotice(
|
||||
"success",
|
||||
payloadData.message || "Vehicle retrieved from the garage.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
bridge.on("garage::retrieve::failure", (payloadData) => {
|
||||
store.finishAction();
|
||||
if (GarageApp.actions) {
|
||||
GarageApp.actions.showNotice(
|
||||
"error",
|
||||
payloadData.message || "Unable to retrieve vehicle.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
bridge.on("garage::store::success", (payloadData) => {
|
||||
store.finishAction();
|
||||
if (GarageApp.actions) {
|
||||
GarageApp.actions.showNotice(
|
||||
"success",
|
||||
payloadData.message || "Vehicle stored in the garage.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
bridge.on("garage::store::failure", (payloadData) => {
|
||||
store.finishAction();
|
||||
if (GarageApp.actions) {
|
||||
GarageApp.actions.showNotice(
|
||||
"error",
|
||||
payloadData.message || "Unable to store vehicle.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
GarageApp.bridge = {
|
||||
notifyReady,
|
||||
receive: bridge.receive,
|
||||
requestClose,
|
||||
requestRefresh,
|
||||
requestRetrieve,
|
||||
requestStore,
|
||||
sendEvent: bridge.send,
|
||||
};
|
||||
})();
|
||||
827
arma/client/addons/garage/ui/src/components/AppShell.js
Normal file
827
arma/client/addons/garage/ui/src/components/AppShell.js
Normal file
@ -0,0 +1,827 @@
|
||||
(function () {
|
||||
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
||||
const { h } = GarageApp.runtime;
|
||||
const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
|
||||
const store = GarageApp.store;
|
||||
const actions = GarageApp.actions;
|
||||
const { categories, garage, nearby, session } = GarageApp.data;
|
||||
|
||||
function q(query, values) {
|
||||
const needle = String(query || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!needle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return values.some((value) =>
|
||||
String(value || "")
|
||||
.toLowerCase()
|
||||
.includes(needle),
|
||||
);
|
||||
}
|
||||
|
||||
function pct(value) {
|
||||
return Math.max(0, Math.min(100, Math.round(Number(value || 0) * 100)));
|
||||
}
|
||||
|
||||
function categoryLabel(category) {
|
||||
const match = categories.find(
|
||||
(entry) => entry.id === String(category || "other").toLowerCase(),
|
||||
);
|
||||
return match ? match.label : "Other";
|
||||
}
|
||||
|
||||
function distanceLabel(value) {
|
||||
return `${Math.round(Number(value || 0))} m`;
|
||||
}
|
||||
|
||||
function plateLabel(value) {
|
||||
return String(value || "").trim() || "Untracked";
|
||||
}
|
||||
|
||||
function statusLabel(vehicle) {
|
||||
if (!vehicle) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
if (vehicle.entryKind === "stored") {
|
||||
return "Stored";
|
||||
}
|
||||
|
||||
return vehicle.isEmpty === false ? "Crewed" : "Ready";
|
||||
}
|
||||
|
||||
function normalizeHitPointLabel(value) {
|
||||
return String(value || "")
|
||||
.replace(/^Hit/i, "")
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.replace(/_/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sameEntry(left, right) {
|
||||
if (!left || !right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
String(left.entryKind || "") === String(right.entryKind || "") &&
|
||||
String(left.plate || "") === String(right.plate || "") &&
|
||||
String(left.netId || "") === String(right.netId || "")
|
||||
);
|
||||
}
|
||||
|
||||
function selectedEntry(state) {
|
||||
if (state.selectedKind === "stored") {
|
||||
return (
|
||||
(garage.vehicles || []).find(
|
||||
(vehicle) =>
|
||||
String(vehicle.plate || "") === state.selectedId,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
if (state.selectedKind === "nearby") {
|
||||
return (
|
||||
(nearby.vehicles || []).find(
|
||||
(vehicle) =>
|
||||
String(vehicle.netId || "") === state.selectedId,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function visibleVehicles(vehicles, state) {
|
||||
return (vehicles || []).filter((vehicle) => {
|
||||
if (
|
||||
state.categoryFilter !== "all" &&
|
||||
String(vehicle.category || "").toLowerCase() !==
|
||||
state.categoryFilter
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return q(state.searchQuery, [
|
||||
vehicle.displayName,
|
||||
vehicle.classname,
|
||||
vehicle.plate,
|
||||
vehicle.netId,
|
||||
vehicle.category,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function stat(label, value, tone = "") {
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
className: tone
|
||||
? `garage-stat-card is-${tone}`
|
||||
: "garage-stat-card",
|
||||
},
|
||||
h("span", { className: "garage-stat-label" }, label),
|
||||
h("span", { className: "garage-stat-value" }, value),
|
||||
);
|
||||
}
|
||||
|
||||
function meter(label, percent, tone) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "garage-meter" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-meter-label-row" },
|
||||
h("span", { className: "garage-meter-label" }, label),
|
||||
h("span", { className: "garage-meter-value" }, `${percent}%`),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-meter-track" },
|
||||
h("span", {
|
||||
className: `garage-meter-fill is-${tone}`,
|
||||
style: { width: `${percent}%` },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function vehicleItem(vehicle, currentSelection) {
|
||||
const id =
|
||||
vehicle.entryKind === "stored"
|
||||
? String(vehicle.plate || "")
|
||||
: String(vehicle.netId || "");
|
||||
const isNearby = vehicle.entryKind === "nearby";
|
||||
|
||||
return h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: sameEntry(vehicle, currentSelection)
|
||||
? "garage-vehicle-item is-selected"
|
||||
: "garage-vehicle-item",
|
||||
onClick: () => actions.selectEntry(vehicle.entryKind, id),
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-vehicle-item-head" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-vehicle-copy" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-vehicle-title" },
|
||||
vehicle.displayName || vehicle.classname || "Vehicle",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-vehicle-meta" },
|
||||
isNearby
|
||||
? `Nearby ${distanceLabel(vehicle.distance)}`
|
||||
: `Plate ${plateLabel(vehicle.plate)}`,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
isNearby && vehicle.isEmpty === false
|
||||
? "garage-badge is-warning"
|
||||
: "garage-badge",
|
||||
},
|
||||
isNearby
|
||||
? vehicle.isEmpty === false
|
||||
? "Crewed"
|
||||
: "Empty"
|
||||
: categoryLabel(vehicle.category),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-inline-meters" },
|
||||
meter("Health", pct(vehicle.health), "health"),
|
||||
meter("Fuel", pct(vehicle.fuel), "fuel"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function vehicleList(title, eyebrow, scrollId, vehicles, currentSelection) {
|
||||
return h(
|
||||
"section",
|
||||
{ className: "garage-card garage-list-card" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-card-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "garage-eyebrow" }, eyebrow),
|
||||
h("h2", { className: "garage-section-title" }, title),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-pill" },
|
||||
`${vehicles.length} ${vehicles.length === 1 ? "Vehicle" : "Vehicles"}`,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className: "garage-card-body garage-scroll-body",
|
||||
"data-preserve-scroll-id": scrollId,
|
||||
},
|
||||
vehicles.length > 0
|
||||
? vehicles.map((vehicle) =>
|
||||
vehicleItem(vehicle, currentSelection),
|
||||
)
|
||||
: h(
|
||||
"div",
|
||||
{ className: "garage-empty-state" },
|
||||
h(
|
||||
"h3",
|
||||
{ className: "garage-empty-title" },
|
||||
"No matching vehicles",
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{ className: "garage-empty-copy" },
|
||||
"Adjust the current search or category filter to view more records.",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function hitPointRows(hitPoints) {
|
||||
const rows = (Array.isArray(hitPoints) ? hitPoints : [])
|
||||
.slice()
|
||||
.sort(
|
||||
(left, right) =>
|
||||
Number(right.value || 0) - Number(left.value || 0),
|
||||
)
|
||||
.slice(0, 6)
|
||||
.filter((row) => Number(row.value || 0) > 0);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return h(
|
||||
"div",
|
||||
{ className: "garage-empty-inline" },
|
||||
"No subsystem damage reported.",
|
||||
);
|
||||
}
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "garage-hitpoint-grid" },
|
||||
rows.map((row) =>
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-hitpoint-row" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-hitpoint-copy" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-hitpoint-name" },
|
||||
normalizeHitPointLabel(row.name) || "Subsystem",
|
||||
),
|
||||
row.selection
|
||||
? h(
|
||||
"span",
|
||||
{ className: "garage-hitpoint-selection" },
|
||||
row.selection,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-hitpoint-value" },
|
||||
`${Math.round(Number(row.value || 0) * 100)}%`,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function detailPanel(currentSelection, state) {
|
||||
if (!currentSelection) {
|
||||
return h(
|
||||
"section",
|
||||
{ className: "garage-card garage-detail-card" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-card-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "garage-eyebrow" }, "Selection"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "garage-section-title" },
|
||||
"Vehicle Detail",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-card-body garage-detail-empty" },
|
||||
h(
|
||||
"h3",
|
||||
{ className: "garage-empty-title" },
|
||||
"Select a vehicle",
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{ className: "garage-empty-copy" },
|
||||
"Choose a stored record to retrieve or a nearby vehicle to store.",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const isStored = currentSelection.entryKind === "stored";
|
||||
const pendingAction = String(state.pendingAction || "");
|
||||
const isBusy =
|
||||
pendingAction === "retrieve" || pendingAction === "store";
|
||||
const canRetrieve = isStored && !session.spawnBlocked && !isBusy;
|
||||
const canStore =
|
||||
!isStored && currentSelection.isEmpty !== false && !isBusy;
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ className: "garage-card garage-detail-card" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-card-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-eyebrow" },
|
||||
isStored ? "Stored Record" : "Nearby Vehicle",
|
||||
),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "garage-section-title" },
|
||||
currentSelection.displayName ||
|
||||
currentSelection.classname ||
|
||||
"Vehicle",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
currentSelection.entryKind === "nearby" &&
|
||||
currentSelection.isEmpty === false
|
||||
? "garage-badge is-warning"
|
||||
: "garage-badge",
|
||||
},
|
||||
isStored
|
||||
? `Plate ${plateLabel(currentSelection.plate)}`
|
||||
: currentSelection.isEmpty === false
|
||||
? "Crewed"
|
||||
: "Ready",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-card-body garage-detail-body" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-detail-grid" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-detail-copy" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-detail-meta" },
|
||||
stat(
|
||||
"Category",
|
||||
categoryLabel(currentSelection.category),
|
||||
),
|
||||
stat(
|
||||
"Status",
|
||||
statusLabel(currentSelection),
|
||||
currentSelection.entryKind === "nearby" &&
|
||||
currentSelection.isEmpty === false
|
||||
? "danger"
|
||||
: "",
|
||||
),
|
||||
stat(
|
||||
isStored ? "Record" : "Distance",
|
||||
isStored
|
||||
? plateLabel(currentSelection.plate)
|
||||
: distanceLabel(currentSelection.distance),
|
||||
isStored ? "" : "accent",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-meter-stack" },
|
||||
meter(
|
||||
"Health",
|
||||
pct(currentSelection.health),
|
||||
"health",
|
||||
),
|
||||
meter("Fuel", pct(currentSelection.fuel), "fuel"),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-action-row" },
|
||||
isStored
|
||||
? h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
"garage-btn garage-btn-primary",
|
||||
disabled: !canRetrieve,
|
||||
onClick: () =>
|
||||
actions.requestRetrieveSelected(),
|
||||
},
|
||||
pendingAction === "retrieve"
|
||||
? "Retrieving..."
|
||||
: "Retrieve Vehicle",
|
||||
)
|
||||
: h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
"garage-btn garage-btn-primary",
|
||||
disabled: !canStore,
|
||||
onClick: () =>
|
||||
actions.requestStoreSelected(),
|
||||
},
|
||||
pendingAction === "store"
|
||||
? "Storing..."
|
||||
: "Store Vehicle",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
"garage-btn garage-btn-secondary",
|
||||
disabled: isBusy,
|
||||
onClick: () => actions.refreshGarage(),
|
||||
},
|
||||
"Refresh",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"p",
|
||||
{ className: "garage-detail-note" },
|
||||
isStored
|
||||
? session.spawnBlocked
|
||||
? "The garage spawn lane is currently blocked."
|
||||
: "Retrieve this stored vehicle into the active spawn lane."
|
||||
: currentSelection.isEmpty === false
|
||||
? "Only empty nearby vehicles can be stored."
|
||||
: "Store this nearby vehicle back into persistent garage storage.",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-detail-subsystems" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-subsystem-header" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-eyebrow" },
|
||||
"Subsystems",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-detail-caption" },
|
||||
"Highest damage first",
|
||||
),
|
||||
),
|
||||
hitPointRows(currentSelection.hitPoints),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
GarageApp.components = GarageApp.components || {};
|
||||
GarageApp.components.App = function App() {
|
||||
const state = {
|
||||
categoryFilter: store.getCategoryFilter(),
|
||||
notice: store.getNotice(),
|
||||
pendingAction: store.getPendingAction(),
|
||||
searchQuery: store.getSearchQuery(),
|
||||
selectedId: store.getSelectedId(),
|
||||
selectedKind: store.getSelectedKind(),
|
||||
};
|
||||
const currentSelection = selectedEntry(state);
|
||||
const storedVehicles = visibleVehicles(garage.vehicles || [], state);
|
||||
const nearbyVehicles = visibleVehicles(nearby.vehicles || [], state);
|
||||
const searchLabel = state.searchQuery
|
||||
? `Search: ${state.searchQuery}`
|
||||
: "Live";
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ className: "garage-shell" },
|
||||
WindowTitleBar({
|
||||
kicker: "FORGE Logistics",
|
||||
title: "Vehicle Garage",
|
||||
onClose: () => actions.closeGarage(),
|
||||
closeLabel: "Close garage interface",
|
||||
}),
|
||||
state.notice.text
|
||||
? h(
|
||||
"div",
|
||||
{ className: "garage-toast-stack" },
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
state.notice.type === "error"
|
||||
? "garage-toast is-error"
|
||||
: "garage-toast is-success",
|
||||
},
|
||||
state.notice.text,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-layout" },
|
||||
h(
|
||||
"aside",
|
||||
{ className: "garage-sidebar" },
|
||||
h(
|
||||
"section",
|
||||
{ className: "garage-module" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-module-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-eyebrow" },
|
||||
"Search",
|
||||
),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "garage-section-title" },
|
||||
"Vehicle Records",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-pill" },
|
||||
searchLabel,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-search-form" },
|
||||
h("input", {
|
||||
id: "garage-search-input",
|
||||
type: "text",
|
||||
className: "garage-search-input",
|
||||
placeholder:
|
||||
"Search by name, plate, or category",
|
||||
value: state.searchQuery,
|
||||
}),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-search-actions" },
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
"garage-btn garage-btn-primary",
|
||||
onClick: () =>
|
||||
actions.applySearchQuery(
|
||||
document.getElementById(
|
||||
"garage-search-input",
|
||||
)?.value || "",
|
||||
),
|
||||
},
|
||||
"Apply Search",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
"garage-btn garage-btn-secondary",
|
||||
onClick: () => actions.clearSearch(),
|
||||
},
|
||||
"Clear",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "garage-module" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-module-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-eyebrow" },
|
||||
"Filter",
|
||||
),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "garage-section-title" },
|
||||
"Vehicle Categories",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-category-grid" },
|
||||
categories.map((category) =>
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
state.categoryFilter === category.id
|
||||
? "garage-chip is-active"
|
||||
: "garage-chip",
|
||||
onClick: () =>
|
||||
actions.selectCategory(category.id),
|
||||
},
|
||||
category.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "garage-module" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-module-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-eyebrow" },
|
||||
"Status",
|
||||
),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "garage-section-title" },
|
||||
"Garage Summary",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className:
|
||||
"garage-btn garage-btn-secondary",
|
||||
disabled: Boolean(state.pendingAction),
|
||||
onClick: () => actions.refreshGarage(),
|
||||
},
|
||||
"Refresh",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-summary-grid" },
|
||||
stat(
|
||||
"Stored",
|
||||
`${session.capacityUsed}/${session.capacityMax}`,
|
||||
),
|
||||
stat("Nearby", session.nearbyCount, "accent"),
|
||||
stat(
|
||||
"Spawn Lane",
|
||||
session.spawnStatus,
|
||||
session.spawnBlocked ? "danger" : "",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"main",
|
||||
{ className: "garage-main" },
|
||||
h(
|
||||
"section",
|
||||
{ className: "garage-panel" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-panel-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-eyebrow" },
|
||||
"Operations Bay",
|
||||
),
|
||||
h(
|
||||
"h1",
|
||||
{ className: "garage-title" },
|
||||
session.garageName || "Vehicle Garage",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-pill" },
|
||||
`${session.capacityUsed}/${session.capacityMax} Stored`,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-panel-intro" },
|
||||
h(
|
||||
"p",
|
||||
{ className: "garage-copy" },
|
||||
"Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-dashboard" },
|
||||
vehicleList(
|
||||
"Stored Vehicles",
|
||||
"Persistent Records",
|
||||
"garage-stored-list",
|
||||
storedVehicles,
|
||||
currentSelection,
|
||||
),
|
||||
vehicleList(
|
||||
"Nearby Vehicles",
|
||||
"Store Window",
|
||||
"garage-nearby-list",
|
||||
nearbyVehicles,
|
||||
currentSelection,
|
||||
),
|
||||
detailPanel(currentSelection, state),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"footer",
|
||||
{ className: "garage-footer" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-footer-block" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-footer-title" },
|
||||
"Storage Capacity",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-footer-copy" },
|
||||
`${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`,
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-footer-block" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-footer-title" },
|
||||
"Retrieval Window",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-footer-copy" },
|
||||
session.spawnBlocked
|
||||
? "Spawn lane is blocked. Clear the bay before retrieving another vehicle."
|
||||
: "Spawn lane is clear. Stored vehicles can be retrieved immediately.",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "garage-footer-block" },
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-footer-title" },
|
||||
"Store Rules",
|
||||
),
|
||||
h(
|
||||
"span",
|
||||
{ className: "garage-footer-copy" },
|
||||
"Only nearby empty vehicles can be stored. Nearby count updates from the live world state.",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
})();
|
||||
57
arma/client/addons/garage/ui/src/data.js
Normal file
57
arma/client/addons/garage/ui/src/data.js
Normal file
@ -0,0 +1,57 @@
|
||||
(function () {
|
||||
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
||||
|
||||
const defaultSession = {
|
||||
garageName: "Vehicle Garage",
|
||||
capacityUsed: 0,
|
||||
capacityMax: 5,
|
||||
nearbyCount: 0,
|
||||
spawnBlocked: false,
|
||||
spawnStatus: "Ready",
|
||||
};
|
||||
|
||||
const defaultGarage = {
|
||||
vehicles: [],
|
||||
};
|
||||
|
||||
const defaultNearby = {
|
||||
vehicles: [],
|
||||
};
|
||||
|
||||
function cloneValue(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function replaceObject(target, source) {
|
||||
Object.keys(target).forEach((key) => delete target[key]);
|
||||
Object.assign(target, cloneValue(source));
|
||||
}
|
||||
|
||||
GarageApp.data = {
|
||||
categories: [
|
||||
{ id: "all", label: "All" },
|
||||
{ id: "car", label: "Cars" },
|
||||
{ id: "armor", label: "Armor" },
|
||||
{ id: "air", label: "Air" },
|
||||
{ id: "naval", label: "Naval" },
|
||||
{ id: "other", label: "Other" },
|
||||
],
|
||||
session: Object.assign({}, defaultSession),
|
||||
garage: Object.assign({}, defaultGarage),
|
||||
nearby: Object.assign({}, defaultNearby),
|
||||
applyHydratePayload(payload) {
|
||||
replaceObject(
|
||||
this.session,
|
||||
Object.assign({}, defaultSession, payload?.session || {}),
|
||||
);
|
||||
replaceObject(
|
||||
this.garage,
|
||||
Object.assign({}, defaultGarage, payload?.garage || {}),
|
||||
);
|
||||
replaceObject(
|
||||
this.nearby,
|
||||
Object.assign({}, defaultNearby, payload?.nearby || {}),
|
||||
);
|
||||
},
|
||||
};
|
||||
})();
|
||||
174
arma/client/addons/garage/ui/src/registry/events.js
Normal file
174
arma/client/addons/garage/ui/src/registry/events.js
Normal file
@ -0,0 +1,174 @@
|
||||
(function () {
|
||||
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
||||
const store = GarageApp.store;
|
||||
|
||||
let noticeTimer = null;
|
||||
|
||||
function getStoredVehicles() {
|
||||
return Array.isArray(GarageApp.data?.garage?.vehicles)
|
||||
? GarageApp.data.garage.vehicles
|
||||
: [];
|
||||
}
|
||||
|
||||
function getNearbyVehicles() {
|
||||
return Array.isArray(GarageApp.data?.nearby?.vehicles)
|
||||
? GarageApp.data.nearby.vehicles
|
||||
: [];
|
||||
}
|
||||
|
||||
function getSelectedEntry() {
|
||||
const selection = store.getSelection();
|
||||
if (selection.kind === "stored") {
|
||||
return (
|
||||
getStoredVehicles().find(
|
||||
(vehicle) => String(vehicle.plate || "") === selection.id,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
if (selection.kind === "nearby") {
|
||||
return (
|
||||
getNearbyVehicles().find(
|
||||
(vehicle) => String(vehicle.netId || "") === selection.id,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function showNotice(type, text) {
|
||||
store.setNotice({ type, text });
|
||||
|
||||
if (noticeTimer) {
|
||||
clearTimeout(noticeTimer);
|
||||
}
|
||||
|
||||
noticeTimer = setTimeout(() => {
|
||||
store.setNotice({ type: "", text: "" });
|
||||
noticeTimer = null;
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
function closeGarage() {
|
||||
const bridge = GarageApp.bridge;
|
||||
if (bridge && typeof bridge.requestClose === "function") {
|
||||
const sent = bridge.requestClose();
|
||||
if (sent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
showNotice("error", "Garage bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
function refreshGarage() {
|
||||
const bridge = GarageApp.bridge;
|
||||
if (bridge && typeof bridge.requestRefresh === "function") {
|
||||
const sent = bridge.requestRefresh();
|
||||
if (sent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
showNotice("error", "Garage refresh bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
function applySearchQuery(value) {
|
||||
store.setSearchQuery(String(value || "").trim());
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
store.setSearchQuery("");
|
||||
}
|
||||
|
||||
function selectCategory(categoryId) {
|
||||
store.setCategoryFilter(String(categoryId || "all").trim() || "all");
|
||||
}
|
||||
|
||||
function selectEntry(kind, id) {
|
||||
store.select(kind, id);
|
||||
}
|
||||
|
||||
function requestRetrieveSelected() {
|
||||
const selectedEntry = getSelectedEntry();
|
||||
if (!selectedEntry || selectedEntry.entryKind !== "stored") {
|
||||
showNotice("error", "Select a stored vehicle to retrieve.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GarageApp.data?.session?.spawnBlocked) {
|
||||
showNotice("error", "The garage spawn area is blocked.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridge = GarageApp.bridge;
|
||||
if (!bridge || typeof bridge.requestRetrieve !== "function") {
|
||||
showNotice("error", "Garage retrieve bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("retrieve");
|
||||
const sent = bridge.requestRetrieve({
|
||||
plate: selectedEntry.plate || "",
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Garage retrieve bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestStoreSelected() {
|
||||
const selectedEntry = getSelectedEntry();
|
||||
if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
|
||||
showNotice("error", "Select a nearby vehicle to store.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedEntry.isEmpty === false) {
|
||||
showNotice(
|
||||
"error",
|
||||
"All crew must exit the vehicle before storing it.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bridge = GarageApp.bridge;
|
||||
if (!bridge || typeof bridge.requestStore !== "function") {
|
||||
showNotice("error", "Garage store bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("store");
|
||||
const sent = bridge.requestStore({
|
||||
netId: selectedEntry.netId || "",
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Garage store bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
GarageApp.actions = {
|
||||
showNotice,
|
||||
closeGarage,
|
||||
refreshGarage,
|
||||
applySearchQuery,
|
||||
clearSearch,
|
||||
selectCategory,
|
||||
selectEntry,
|
||||
getSelectedEntry,
|
||||
requestRetrieveSelected,
|
||||
requestStoreSelected,
|
||||
};
|
||||
})();
|
||||
113
arma/client/addons/garage/ui/src/registry/store.js
Normal file
113
arma/client/addons/garage/ui/src/registry/store.js
Normal file
@ -0,0 +1,113 @@
|
||||
(function () {
|
||||
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
||||
const { createSignal } = GarageApp.runtime;
|
||||
|
||||
class GarageStore {
|
||||
constructor() {
|
||||
[this.getSelectedKind, this.setSelectedKind] = createSignal("");
|
||||
[this.getSelectedId, this.setSelectedId] = createSignal("");
|
||||
[this.getSearchQuery, this.setSearchQuery] = createSignal("");
|
||||
[this.getCategoryFilter, this.setCategoryFilter] =
|
||||
createSignal("all");
|
||||
[this.getPendingAction, this.setPendingAction] = createSignal("");
|
||||
[this.getNotice, this.setNotice] = createSignal({
|
||||
type: "",
|
||||
text: "",
|
||||
});
|
||||
}
|
||||
|
||||
getSelection() {
|
||||
return {
|
||||
id: this.getSelectedId(),
|
||||
kind: this.getSelectedKind(),
|
||||
};
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.setSelectedKind("");
|
||||
this.setSelectedId("");
|
||||
}
|
||||
|
||||
select(kind, id) {
|
||||
this.setSelectedKind(String(kind || ""));
|
||||
this.setSelectedId(String(id || ""));
|
||||
}
|
||||
|
||||
startAction(action) {
|
||||
this.setPendingAction(String(action || ""));
|
||||
}
|
||||
|
||||
finishAction() {
|
||||
this.setPendingAction("");
|
||||
}
|
||||
|
||||
matchesSelection(entry) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selection = this.getSelection();
|
||||
if (!selection.kind || !selection.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selection.kind === "stored") {
|
||||
return (
|
||||
entry.entryKind === "stored" &&
|
||||
String(entry.plate || "") === selection.id
|
||||
);
|
||||
}
|
||||
|
||||
if (selection.kind === "nearby") {
|
||||
return (
|
||||
entry.entryKind === "nearby" &&
|
||||
String(entry.netId || "") === selection.id
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ensureSelection() {
|
||||
const garageVehicles = Array.isArray(
|
||||
GarageApp.data?.garage?.vehicles,
|
||||
)
|
||||
? GarageApp.data.garage.vehicles
|
||||
: [];
|
||||
const nearbyVehicles = Array.isArray(
|
||||
GarageApp.data?.nearby?.vehicles,
|
||||
)
|
||||
? GarageApp.data.nearby.vehicles
|
||||
: [];
|
||||
const hasCurrentSelection = [
|
||||
...garageVehicles,
|
||||
...nearbyVehicles,
|
||||
].some((entry) => this.matchesSelection(entry));
|
||||
|
||||
if (hasCurrentSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstStored = garageVehicles[0] || null;
|
||||
if (firstStored) {
|
||||
this.select("stored", firstStored.plate || "");
|
||||
return;
|
||||
}
|
||||
|
||||
const firstNearby = nearbyVehicles[0] || null;
|
||||
if (firstNearby) {
|
||||
this.select("nearby", firstNearby.netId || "");
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
hydrateFromPayload() {
|
||||
this.finishAction();
|
||||
this.ensureSelection();
|
||||
}
|
||||
}
|
||||
|
||||
GarageApp.store = new GarageStore();
|
||||
})();
|
||||
7
arma/client/addons/garage/ui/src/runtime.js
Normal file
7
arma/client/addons/garage/ui/src/runtime.js
Normal file
@ -0,0 +1,7 @@
|
||||
(function () {
|
||||
const runtime = window.ForgeWebUI;
|
||||
const GarageApp = (window.GarageApp = window.GarageApp || {});
|
||||
|
||||
GarageApp.runtime = runtime;
|
||||
window.AppRuntime = runtime;
|
||||
})();
|
||||
573
arma/client/addons/garage/ui/src/styles.css
Normal file
573
arma/client/addons/garage/ui/src/styles.css
Normal file
@ -0,0 +1,573 @@
|
||||
:root {
|
||||
--garage-shell-bg: #e4e3df;
|
||||
--garage-surface: #f5f3ef;
|
||||
--garage-surface-alt: #ece8e2;
|
||||
--garage-border: rgba(74, 91, 110, 0.2);
|
||||
--garage-border-strong: rgba(20, 46, 79, 0.18);
|
||||
--garage-text-main: #1f2d3d;
|
||||
--garage-text-muted: #6a7787;
|
||||
--garage-text-subtle: #8792a0;
|
||||
--garage-accent: #12365d;
|
||||
--garage-accent-soft: #dbe7f3;
|
||||
--garage-accent-line: rgba(18, 54, 93, 0.12);
|
||||
--garage-warning: #8f5f26;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", "Trebuchet MS", sans-serif;
|
||||
color: var(--garage-text-main);
|
||||
background: var(--garage-shell-bg);
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid rgb(18 54 93 / 0.35);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.garage-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--garage-shell-bg);
|
||||
}
|
||||
|
||||
.garage-layout {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: min(100%, 1613px);
|
||||
margin: 0 auto;
|
||||
padding: 1.25rem;
|
||||
display: grid;
|
||||
grid-template-columns: 308px minmax(0, 1fr);
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.garage-sidebar,
|
||||
.garage-main {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.garage-main {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.garage-module,
|
||||
.garage-panel,
|
||||
.garage-card {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
var(--garage-surface) 0%,
|
||||
var(--garage-surface-alt) 100%
|
||||
);
|
||||
border: 1px solid var(--garage-border);
|
||||
border-radius: 1.35rem;
|
||||
}
|
||||
|
||||
.garage-module,
|
||||
.garage-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.garage-module {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.garage-panel {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.garage-panel-header,
|
||||
.garage-module-header,
|
||||
.garage-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.garage-panel-header {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.garage-module-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.garage-panel-intro {
|
||||
padding: 0 1rem 1rem;
|
||||
border-bottom: 1px solid var(--garage-accent-line);
|
||||
}
|
||||
|
||||
.garage-dashboard {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.garage-list-card,
|
||||
.garage-detail-card {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.garage-detail-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.garage-scroll-body {
|
||||
flex: 1;
|
||||
min-height: 20rem;
|
||||
max-height: 24rem;
|
||||
overflow: auto;
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
padding-right: 0.2rem;
|
||||
}
|
||||
|
||||
.garage-detail-body {
|
||||
padding-top: 0.95rem;
|
||||
}
|
||||
|
||||
.garage-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.85fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.garage-detail-meta,
|
||||
.garage-summary-grid,
|
||||
.garage-search-actions,
|
||||
.garage-category-grid,
|
||||
.garage-action-row,
|
||||
.garage-inline-meters,
|
||||
.garage-hitpoint-grid,
|
||||
.garage-footer {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.garage-detail-meta {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.garage-summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.garage-summary-grid > :last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.garage-search-actions,
|
||||
.garage-action-row {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.garage-category-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.garage-footer {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
padding: 0.95rem 1.25rem 1.15rem;
|
||||
border-top: 1px solid rgb(18 54 93 / 0.1);
|
||||
}
|
||||
|
||||
.garage-meter-stack {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.garage-eyebrow,
|
||||
.garage-footer-title,
|
||||
.garage-stat-label,
|
||||
.garage-meter-label,
|
||||
.garage-hitpoint-selection {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--garage-text-subtle);
|
||||
}
|
||||
|
||||
.garage-title,
|
||||
.garage-section-title {
|
||||
margin: 0.16rem 0 0;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.garage-section-title {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.garage-copy,
|
||||
.garage-detail-note,
|
||||
.garage-empty-copy,
|
||||
.garage-footer-copy,
|
||||
.garage-vehicle-meta,
|
||||
.garage-detail-caption {
|
||||
margin: 0;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.48;
|
||||
color: var(--garage-text-muted);
|
||||
}
|
||||
|
||||
.garage-pill,
|
||||
.garage-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.48rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
background: var(--garage-accent-soft);
|
||||
color: var(--garage-accent);
|
||||
}
|
||||
|
||||
.garage-badge.is-warning {
|
||||
background: rgb(246 226 193 / 0.88);
|
||||
color: var(--garage-warning);
|
||||
}
|
||||
|
||||
.garage-search-form {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.garage-search-input {
|
||||
width: 100%;
|
||||
height: 2.9rem;
|
||||
padding: 0 0.95rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.75);
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-stat-card {
|
||||
min-width: 0;
|
||||
padding: 0.85rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.48);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.garage-stat-card.is-accent {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(237 243 249 / 0.92) 0%,
|
||||
rgb(223 232 242 / 0.72) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.garage-stat-card.is-danger {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(254 242 242 / 0.95) 0%,
|
||||
rgb(252 225 225 / 0.82) 100%
|
||||
);
|
||||
border-color: rgb(220 151 151 / 0.38);
|
||||
}
|
||||
|
||||
.garage-stat-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
line-height: 1.3;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.garage-chip {
|
||||
min-height: 2.6rem;
|
||||
padding: 0.68rem 0.9rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.52);
|
||||
color: var(--garage-text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.garage-chip.is-active {
|
||||
background: var(--garage-accent-soft);
|
||||
color: var(--garage-accent);
|
||||
border-color: rgb(18 54 93 / 0.2);
|
||||
}
|
||||
|
||||
.garage-vehicle-item {
|
||||
width: 100%;
|
||||
padding: 0.9rem;
|
||||
border-radius: 0.95rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.48);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.garage-vehicle-item.is-selected {
|
||||
border-color: rgb(18 54 93 / 0.24);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgb(237 243 249 / 0.96) 0%,
|
||||
rgb(223 232 242 / 0.74) 100%
|
||||
);
|
||||
box-shadow: 0 16px 26px rgb(18 54 93 / 0.08);
|
||||
}
|
||||
|
||||
.garage-vehicle-item-head,
|
||||
.garage-meter-label-row,
|
||||
.garage-subsystem-header,
|
||||
.garage-hitpoint-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.garage-vehicle-copy,
|
||||
.garage-hitpoint-copy,
|
||||
.garage-footer-block {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
.garage-vehicle-title,
|
||||
.garage-hitpoint-name,
|
||||
.garage-hitpoint-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-meter {
|
||||
display: grid;
|
||||
gap: 0.32rem;
|
||||
}
|
||||
|
||||
.garage-meter-track {
|
||||
width: 100%;
|
||||
height: 0.45rem;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgb(18 54 93 / 0.08);
|
||||
}
|
||||
|
||||
.garage-meter-value {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-meter-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.garage-meter-fill.is-health {
|
||||
background: linear-gradient(90deg, #2f7d5b 0%, #4eaa82 100%);
|
||||
}
|
||||
|
||||
.garage-meter-fill.is-fuel {
|
||||
background: linear-gradient(90deg, #12365d 0%, #3c6792 100%);
|
||||
}
|
||||
|
||||
.garage-btn {
|
||||
min-height: 2.75rem;
|
||||
padding: 0.72rem 1rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--garage-border-strong);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.garage-btn-primary {
|
||||
background: rgb(255 255 255 / 0.68);
|
||||
color: var(--garage-accent);
|
||||
}
|
||||
|
||||
.garage-btn-primary:hover {
|
||||
background: rgb(219 231 243 / 0.88);
|
||||
}
|
||||
|
||||
.garage-btn-secondary {
|
||||
background: rgb(255 255 255 / 0.42);
|
||||
color: var(--garage-text-muted);
|
||||
}
|
||||
|
||||
.garage-btn-secondary:hover {
|
||||
background: rgb(255 255 255 / 0.6);
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-hitpoint-row {
|
||||
padding: 0.72rem 0.78rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: rgb(255 255 255 / 0.52);
|
||||
}
|
||||
|
||||
.garage-detail-empty,
|
||||
.garage-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.garage-empty-title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--garage-text-main);
|
||||
}
|
||||
|
||||
.garage-empty-inline {
|
||||
padding: 0.9rem;
|
||||
border-radius: 0.85rem;
|
||||
border: 1px dashed var(--garage-border);
|
||||
color: var(--garage-text-muted);
|
||||
background: rgb(255 255 255 / 0.36);
|
||||
}
|
||||
|
||||
.garage-toast-stack {
|
||||
position: fixed;
|
||||
top: 1.2rem;
|
||||
right: 1.5rem;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.garage-toast {
|
||||
max-width: 24rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--garage-border);
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 28px rgb(16 34 56 / 0.14);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.garage-toast.is-success {
|
||||
background: #ecfdf5;
|
||||
border-color: #bbf7d0;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.garage-toast.is-error {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.garage-layout {
|
||||
grid-template-columns: 288px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.garage-detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.garage-layout {
|
||||
grid-template-columns: 1fr;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.garage-main,
|
||||
.garage-sidebar {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.garage-dashboard {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.garage-detail-card {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.garage-scroll-body {
|
||||
max-height: none;
|
||||
min-height: 16rem;
|
||||
}
|
||||
|
||||
.garage-footer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
33
arma/client/addons/garage/ui/ui.config.mjs
Normal file
33
arma/client/addons/garage/ui/ui.config.mjs
Normal file
@ -0,0 +1,33 @@
|
||||
export default {
|
||||
addonName: "garage",
|
||||
title: "FORGE Vehicle Garage",
|
||||
logLabel: "Garage UI",
|
||||
outputDir: "_site",
|
||||
jsBundles: [
|
||||
{
|
||||
name: "Garage UI app",
|
||||
output: "garage-ui.js",
|
||||
sources: [
|
||||
"src/runtime.js",
|
||||
"src/data.js",
|
||||
"src/registry/store.js",
|
||||
"src/bridge.js",
|
||||
"src/registry/events.js",
|
||||
"src/components/AppShell.js",
|
||||
"src/bootstrap.js",
|
||||
],
|
||||
},
|
||||
],
|
||||
cssBundles: [
|
||||
{
|
||||
name: "Garage UI styles",
|
||||
output: "garage-ui.css",
|
||||
sources: ["src/styles.css"],
|
||||
},
|
||||
],
|
||||
site: {
|
||||
styles: ["garage-ui.css"],
|
||||
commonScripts: ["forge-webui.js"],
|
||||
scripts: ["garage-ui.js"],
|
||||
},
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initOrgClass);
|
||||
PREP(initOrgUIBridge);
|
||||
PREP(initClass);
|
||||
PREP(initUIBridge);
|
||||
PREP(openUI);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); };
|
||||
if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); };
|
||||
if (isNil QGVAR(OrgClass)) then { call FUNC(initClass); };
|
||||
if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(initOrg), {
|
||||
GVAR(OrgClass) call ["init", []];
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initOrgClass.sqf
|
||||
* File: fnc_initClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-02-13
|
||||
* Last Update: 2026-02-13
|
||||
@ -17,7 +17,7 @@
|
||||
* Org class object [HASHMAP OBJECT]
|
||||
*
|
||||
* Examples:
|
||||
* call forge_client_org_fnc_initOrgClass
|
||||
* call forge_client_org_fnc_initClass
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
@ -1,7 +1,7 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initOrgUIBridge.sqf
|
||||
* File: fnc_initUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-10
|
||||
* Last Update: 2026-03-13
|
||||
@ -17,7 +17,7 @@
|
||||
* Org UI bridge object [HASHMAP OBJECT]
|
||||
*
|
||||
* Examples:
|
||||
* call forge_client_org_fnc_initOrgUIBridge
|
||||
* call forge_client_org_fnc_initUIBridge
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
@ -1,6 +1,6 @@
|
||||
PREP(buildStoreUIPayload);
|
||||
PREP(buildUIPayload);
|
||||
PREP(handleUIEvents);
|
||||
PREP(initStoreCatalogService);
|
||||
PREP(initStoreClass);
|
||||
PREP(initStoreUIBridge);
|
||||
PREP(initCatalogService);
|
||||
PREP(initClass);
|
||||
PREP(initUIBridge);
|
||||
PREP(openUI);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initStoreCatalogService); };
|
||||
if (isNil QGVAR(StoreClass)) then { call FUNC(initStoreClass); };
|
||||
if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initStoreUIBridge); };
|
||||
if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initCatalogService); };
|
||||
if (isNil QGVAR(StoreClass)) then { call FUNC(initClass); };
|
||||
if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(responseCheckout), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_buildStoreUIPayload.sqf
|
||||
* File: fnc_buildUIPayload.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-13
|
||||
* Public: No
|
||||
@ -1,7 +1,7 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initStoreCatalogService.sqf
|
||||
* File: fnc_initCatalogService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-13
|
||||
* Public: No
|
||||
@ -1,7 +1,7 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initStoreClass.sqf
|
||||
* File: fnc_initClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-01-28
|
||||
* Last Update: 2026-03-12
|
||||
@ -17,7 +17,7 @@
|
||||
* Store class object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example:
|
||||
* call forge_client_store_fnc_initStoreClass
|
||||
* call forge_client_store_fnc_initClass
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
@ -1,7 +1,7 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initStoreUIBridge.sqf
|
||||
* File: fnc_initUIBridge.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-10
|
||||
* Last Update: 2026-03-12
|
||||
@ -48,7 +48,7 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
["handleReady", compileFinal {
|
||||
params [["_control", controlNull, [controlNull]]];
|
||||
|
||||
private _payload = call FUNC(buildStoreUIPayload);
|
||||
private _payload = call FUNC(buildUIPayload);
|
||||
_self call ["sendBridgeEvent", ["store::hydrate", _payload, _control]];
|
||||
}],
|
||||
["handleCategoryRequest", compileFinal {
|
||||
@ -78,7 +78,7 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
]]];
|
||||
}],
|
||||
["refreshStoreConfig", compileFinal {
|
||||
private _payload = call FUNC(buildStoreUIPayload);
|
||||
private _payload = call FUNC(buildUIPayload);
|
||||
_self call ["sendBridgeEvent", ["store::config::hydrate", _payload]];
|
||||
}],
|
||||
["handleCheckoutRequest", compileFinal {
|
||||
@ -2778,6 +2778,12 @@ ${scopeSelector} .cart-line {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-line-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-line-top,
|
||||
${scopeSelector} .cart-line-controls,
|
||||
${scopeSelector} .summary-row {
|
||||
@ -2790,13 +2796,9 @@ ${scopeSelector} .summary-row {
|
||||
${scopeSelector} .cart-line-title {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-line-code {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--store-text-subtle);
|
||||
line-height: 1.32;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
${scopeSelector} .qty-controls {
|
||||
@ -3112,15 +3114,9 @@ ${scopeSelector} .cart-empty {
|
||||
{ className: "cart-line-top" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"cart-line-code",
|
||||
},
|
||||
item.code,
|
||||
),
|
||||
{
|
||||
className: "cart-line-copy",
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
|
||||
@ -150,6 +150,12 @@ ${scopeSelector} .cart-line {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-line-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-line-top,
|
||||
${scopeSelector} .cart-line-controls,
|
||||
${scopeSelector} .summary-row {
|
||||
@ -162,13 +168,9 @@ ${scopeSelector} .summary-row {
|
||||
${scopeSelector} .cart-line-title {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
${scopeSelector} .cart-line-code {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--store-text-subtle);
|
||||
line-height: 1.32;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
${scopeSelector} .qty-controls {
|
||||
@ -484,15 +486,9 @@ ${scopeSelector} .cart-empty {
|
||||
{ className: "cart-line-top" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"cart-line-code",
|
||||
},
|
||||
item.code,
|
||||
),
|
||||
{
|
||||
className: "cart-line-copy",
|
||||
},
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
|
||||
@ -65,6 +65,84 @@ PREP_RECOMPILE_END;
|
||||
GVAR(GarageStore) call ["remove", [GVAR(Registry), _uid]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestStoreVehicle), {
|
||||
params [
|
||||
["_uid", "", [""]],
|
||||
["_className", "", [""]],
|
||||
["_fuel", 0, [0]],
|
||||
["_damage", 0, [0]],
|
||||
["_hitPointsJson", "", [""]]
|
||||
];
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_uid isEqualTo "" || { _className isEqualTo "" } || { _hitPointsJson isEqualTo "" }) exitWith {
|
||||
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
|
||||
["action", "store"],
|
||||
["success", false],
|
||||
["message", "Missing vehicle data for garage storage."]
|
||||
]], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
private _payloadJson = toJSON (createHashMapFromArray [
|
||||
["classname", _className],
|
||||
["fuel", _fuel],
|
||||
["damage", _damage],
|
||||
["hit_points", fromJSON _hitPointsJson]
|
||||
]);
|
||||
|
||||
["garage:add", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
|
||||
if !(_isSuccess) exitWith {
|
||||
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
|
||||
["action", "store"],
|
||||
["success", false],
|
||||
["message", format ["Failed to store vehicle: %1", _result]]
|
||||
]], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
private _garage = fromJSON _result;
|
||||
GVAR(Registry) set [_uid, _garage];
|
||||
|
||||
[CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent);
|
||||
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
|
||||
["action", "store"],
|
||||
["success", true],
|
||||
["message", "Vehicle stored in garage."]
|
||||
]], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestRetrieveVehicle), {
|
||||
params [["_uid", "", [""]], ["_plate", "", [""]]];
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_uid isEqualTo "" || { _plate isEqualTo "" }) exitWith {
|
||||
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
|
||||
["action", "retrieve"],
|
||||
["success", false],
|
||||
["message", "Select a stored vehicle to retrieve."]
|
||||
]], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
private _payloadJson = toJSON (createHashMapFromArray [["plate", _plate]]);
|
||||
["garage:remove", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
|
||||
if !(_isSuccess) exitWith {
|
||||
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
|
||||
["action", "retrieve"],
|
||||
["success", false],
|
||||
["message", format ["Failed to retrieve vehicle: %1", _result]]
|
||||
]], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
private _garage = fromJSON _result;
|
||||
GVAR(Registry) set [_uid, _garage];
|
||||
|
||||
[CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent);
|
||||
[CRPC(garage,responseGarageAction), [createHashMapFromArray [
|
||||
["action", "retrieve"],
|
||||
["success", true],
|
||||
["message", "Vehicle retrieved from garage."]
|
||||
]], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestInitVG), {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user