Compare commits

...

2 Commits

Author SHA1 Message Date
Jacob Schmidt
771b5f1c32 Merge branch 'master' of https://gitea.innovativedevsolutions.org/IDSolutions/client
All checks were successful
Build / Build (push) Successful in 30s
2025-05-03 19:33:15 -05:00
Jacob Schmidt
55f60dc71f feat: Revamp admin panel, store, and arsenal systems
This commit introduces significant changes to the admin panel, store, and arsenal systems, focusing on improved functionality, UI enhancements, and code optimization.

**Admin Panel:**
- Migrated to a web-based UI for improved user experience and maintainability.
- Implemented dynamic player listing with filtering and search capabilities.
- Added functionality for managing player paygrades, sending messages, and transferring funds.
- Integrated server-side events for handling admin actions.

**Store:**
- Added `handleDelivery` event handler.
- Streamlined product selection and purchase processes.
- Improved handling of organization funds and player balances.
- Refactored code for better readability and maintainability.

**Arsenal:**
- Enhanced initialization process with improved data validation.
- Optimized item unlocking logic.

These changes aim to provide a more robust, user-friendly, and efficient experience for both administrators and players.
2025-05-03 19:33:10 -05:00
28 changed files with 1222 additions and 765 deletions

View File

@ -0,0 +1,23 @@
name: ctrlWebBrowserAction
description: Executes an action on a web browser control
groups:
- GUI Control
syntax:
- call: !Binary [control, action]
ret:
- Nothing
- Nothing
params:
- name: control
type: Control
- name: action
type: String
argument_loc: Local
effect_loc: Local
since:
arma_3:
major: 2
minor: 2
examples:
- <sqf>_control ctrlWebBrowserAction ["ExecJS", "document.getElementById('test').innerHTML = 'Hello World!'"];</sqf>
- <sqf>_control ctrlWebBrowserAction ["LoadURL", "https://community.bistudio.com"];</sqf>

View File

@ -1,7 +1,6 @@
PREP(adminMessage);
PREP(adminPromote);
PREP(adminRefresh);
PREP(adminTransfer);
PREP(handleTransfer);
PREP(initAdmin);
PREP(openAdmin);
PREP(printAddonName);
PREP(sendMessage);
PREP(updatePaygrade);

View File

@ -1 +1,96 @@
#include "script_component.hpp"
[QGVAR(handleEvents), {
params ["_control", "_isConfirmDialog", "_message"];
diag_log format ["[FORGE: Admin] Received event: %1", _message];
_message = fromJSON _message;
private _event = _message get "event";
private _data = _message get "data";
switch (_event) do {
case "REQUEST_PLAYER_DATA": {
private _playerData = createHashMap;
private _playerList = [];
{
private _player = _x;
private _uid = getPlayerUID _player;
private _name = name _player;
private _paygrade = GETVAR(_player,FORGE_PayGrade,QUOTE(E1));
private _funds = GETVAR(_player,FORGE_Bank,0);
private _playerInfo = createHashMapFromArray [
["uid", _uid],
["name", _name],
["paygrade", _paygrade],
["funds", _funds],
["side", str (side _player)]
];
_playerList pushBack _playerInfo;
} forEach allPlayers;
_playerData set ["players", _playerList];
_control ctrlWebBrowserAction ["ExecJS", format ["handlePlayerDataRequest(%1)", (toJSON _playerList)]];
};
case "REQUEST_PAYGRADE_DATA": {
private _payGrades = (missionConfigFile >> "CfgPaygrades" >> "payGrades") call BIS_fnc_getCfgData;
private _paygradeData = createHashMap;
private _paygradeList = [];
{
private _paygradeInfo = createHashMapFromArray [
["paygrade", _x select 0],
["bonus", _x select 1]
];
_paygradeList pushBack _paygradeInfo;
} forEach _payGrades;
_paygradeData set ["paygrades", _paygradeList];
_control ctrlWebBrowserAction ["ExecJS", format ["handlePaygradeDataRequest(%1)", (toJSON _paygradeList)]];
};
case "BROADCAST_MESSAGE": {
_data params ["_uid", "_message"];
if ((isNil "_message") || {_message isEqualTo ""}) exitWith { hintSilent "Message cannot be empty!"; };
["forge_server_admin_handleEvents", ["sendMessage", [_uid, _message]]] call CFUNC(serverEvent);
};
case "SEND_MESSAGE": {
_data params ["_uid", "_message"];
if ((isNil "_uid") || {_uid isEqualTo ""}) exitWith { hintSilent "You did not select a player!"; };
["forge_server_admin_handleEvents", ["sendMessage", [_uid, _message]]] call CFUNC(serverEvent);
};
case "UPDATE_PAYGRADE": {
private _uid = _data select 0;
private _paygrade = _data select 1;
if ((isNil "_uid") || {_uid isEqualTo ""}) exitWith { hintSilent "You did not select a player!"; };
["forge_server_admin_handleEvents", ["updatePaygrade", [_uid, _paygrade]]] call CFUNC(serverEvent);
};
case "HANDLE_TRANSFER": {
private _condition = _data select 0;
private _amount = _data select 1;
private _uid = _data select 2;
["forge_server_admin_handleEvents", ["handleTransfer", [_condition, _amount, _uid]]] call CFUNC(serverEvent);
};
case "ADVANCE_ALL": {
private _amount = _data select 0;
["forge_server_admin_handleEvents", ["advanceAll", [_amount]]] call CFUNC(serverEvent);
};
case "HANDLE_PAYDAY": {
["forge_server_admin_handleEvents", ["handlePayday"]] call CFUNC(serverEvent);
};
default {
diag_log format ["[FORGE: Admin] Unhandled event: %1", _event];
};
};
}] call CFUNC(addEventHandler);

View File

@ -16,3 +16,4 @@ class CfgPatches {
#include "CfgEventHandlers.hpp"
#include "ui\RscCommon.hpp"
#include "ui\RscAdmin.hpp"
#include "ui\RscWebAdmin.hpp"

View File

@ -1,49 +0,0 @@
#include "..\script_component.hpp"
/*
* Function: forge_client_admin_fnc_adminMessage
* Author: IDSolutions
*
* [Description]
* Admin Message Menu
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* None
*
* Public: Yes
*/
private ["_data", "_dialog", "_list", "_target", "_targetValue", "_textBox", "_textMessage"];
_dialog = findDisplay 202303;
_list = _dialog displayCtrl 2023001;
_textBox = _dialog displayCtrl 2023006;
_targetValue = lbCurSel _list;
_data = _list lbData _targetValue;
if ((isNil {_data})) exitWith { hintSilent "You did not select a player!" };
{
if (str (name (_x)) == str _data) then {
_target = _x;
};
} forEach playableUnits;
hintSilent format ["Player Selected. You have selected %1", _target];
if (isNil "_target") then {
hintSilent "Please Select an Active Player First!"
} else {
_textMessage = ctrlText _textBox;
[_target, _textMessage] remoteExec ["forge_server_misc_fnc_textMessage", 2];
// [format ["Message sent to %1: <br/>%2", _target, _textMessage], "blue-grey", 3] call EFUNC(misc,notify);
};
["dummy"] call FUNC(adminRefresh);

View File

@ -1,51 +0,0 @@
#include "..\script_component.hpp"
/*
* Function: forge_client_admin_fnc_adminPromote
* Author: IDSolutions
*
* [Description]
* Admin Promote Menu
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* None
*
* Public: Yes
*/
params [["_condition", "", [""]]];
private ["_data", "_data2", "_dialog", "_list", "_list2", "_paygrade", "_rankValue", "_target", "_targetValue"];
_dialog = findDisplay 202303;
_list = _dialog displayCtrl 2023001;
_list2 = _dialog displayCtrl 2023003;
_targetValue = lbCurSel _list;
_rankValue = lbCurSel _list2;
_data = _list lbData _targetValue;
_data2 = call compile format ["%1", (_list2 lbData _rankValue)];
_paygrade = _data2 select 0;
if ((isNil {_data})) exitWith { hintSilent "You did not select a player!" };
{
if (str (name (_x)) == str _data) then {
_target = _x;
};
} forEach playableUnits;
switch (_condition) do {
case ("promote"): {
SETPVAR(_target,FORGE_PayGrade,_paygrade)
};
case ("demote"): {
SETPVAR(_target,FORGE_PayGrade,_paygrade)
};
};
["dummy"] call FUNC(adminRefresh);

View File

@ -5,74 +5,39 @@
* Author: IDSolutions
*
* [Description]
* Admin Refresh Menu
* Refreshes the admin interface player list and resets input fields.
* This function populates the player list with names and paygrades,
* storing player UIDs as data for each entry. Only shows players
* on the same side as the admin.
*
* Arguments:
* None
* 0: Dummy <ANY> - Optional parameter, not used (for compatibility with event handlers)
*
* Return Value:
* None
*
* Examples:
* None
* [] call forge_client_admin_fnc_adminRefresh;
* ["dummy"] call forge_client_admin_fnc_adminRefresh;
*
* Public: Yes
* Public: No - Called from admin dialog controls
*/
params [["_condition", "", [""]], ["_amount", 0, [0]]];
private _store = missionNamespace getVariable ["FORGE_ORG_STORE_REG", createHashMap];
private _org = _store call ["getOrg", []];
if (isNil "_org") exitWith { ["You are not in an organization!", "warning", 3] call EFUNC(misc,notify) };
private _orgFunds = _org get "funds";
private _newFunds = 0;
private _dialog = findDisplay 202303;
private _list = _dialog displayCtrl 2023001;
switch (_condition) do {
case ("deduct"): {
_newFunds = _orgFunds - _amount;
ctrlSetText [2023002, format ["$%1", (_newFunds call EFUNC(misc,formatNumber))]];
};
case ("advance"): {
_newFunds = _orgFunds + _amount;
ctrlSetText [2023002, format ["$%1", (_newFunds call EFUNC(misc,formatNumber))]];
};
default {
lbClear _list;
{
if (str (side _x) == str (playerSide)) then {
private _name = name (_x);
private _defaultPaygrade = "E1";
private _paygrade = GETVAR(_x,FORGE_PayGrade,_defaultPaygrade);
private _index = _list lbAdd format["%1 - %2", _name, _paygrade];
_list lbSetData [_index, name (_x)];
};
} forEach playableUnits;
lbSetCurSel [2023001, 0];
ctrlSetText [2023005, ""];
ctrlSetText [2023006, ""];
};
};
lbClear _list;
{
if (str (side _x) == str (playerSide)) then {
private _name = name (_x);
private _defaultPaygrade = "E1";
private _paygrade = GETVAR(_x,FORGE_PayGrade,_defaultPaygrade);
private _paygrade = GETVAR(_x,FORGE_PayGrade,QUOTE(E1));
private _index = _list lbAdd format["%1 - %2", _name, _paygrade];
_list lbSetData [_index, name (_x)];
_list lbSetData [_index, getPlayerUID _x];
};
} forEach playableUnits;
} forEach allPlayers;
lbSetCurSel [2023001, 0];
ctrlSetText [2023005, ""];

View File

@ -1,118 +0,0 @@
#include "..\script_component.hpp"
/*
* Function: forge_client_admin_fnc_adminTransfer
* Author: IDSolutions
*
* [Description]
* Admin Transfer Menu
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* None
*
* Public: Yes
*/
params [["_condition", "", [""]]];
private _store = missionNamespace getVariable ["FORGE_ORG_STORE_REG", createHashMap];
private _org = _store call ["getOrg", []];
if (isNil "_org") exitWith { ["You are not in an organization!", "warning", 3] call EFUNC(misc,notify) };
private _orgFunds = _org get "funds";
private _dialog = findDisplay 202303;
private _list = _dialog displayCtrl 2023001;
private _targetValue = lbCurSel _list;
private _data = _list lbData _targetValue;
private _amount = round (parseNumber (ctrlText 2023005));
if ((isNil {_data})) exitWith { hint "You did not select a player!" };
{
if (str (name (_x)) == str _data) then {
private _target = _x;
};
} count playableUnits;
switch (_condition) do {
case ("advance"): {
private _bank = GETVAR(_target,FORGE_Bank,0);
private _newBalance = _bank + _amount;
if (_amount > _orgFunds) exitWith { ["Not enough money in the organization's account!", "warning", 3] call EFUNC(misc,notify) };
SETPVAR(_target,FORGE_Bank,_newBalance);
["deduct", _amount] call FUNC(adminRefresh);
_store call ["updateFunds", -_amount];
};
case ("advanceAll"): {
private _count = count playableUnits;
if ((10000 * _count) > _orgFunds) exitWith { ["Not enough money in the organization's account!", "warning", 3] call EFUNC(misc,notify) };
{
private _bank = GETVAR(_x,FORGE_Bank,0);
private _newBalance = _bank + 10000;
SETPVAR(_x,FORGE_Bank,_newBalance);
} count playableUnits;
["deduct", (10000 * _count)] call FUNC(adminRefresh);
_store call ["updateFunds", -(10000 * _count)];
};
case ("deduct"): {
private _bank = GETVAR(_target,FORGE_Bank,0);
private _newBalance = _bank - _amount;
if (_amount > _bank) exitWith { ["Not enough money in the player's account!", "warning", 3] call EFUNC(misc,notify) };
SETPVAR(_target,FORGE_Bank,_newBalance);
["advance", _amount] call FUNC(adminRefresh);
_store call ["updateFunds", _amount];
};
case ("payday"): {
private _totalPayment = 0;
private _paymentToDo = [];
private _payGrades = (missionConfigFile >> "CfgPaygrades" >> "payGrades") call BIS_fnc_getCfgData;
{
private _player = _x;
private _payGrade = GETVAR(_player,FORGE_PayGrade,nil);
{
_x params ["_payGradeIndex", "_payGradeBonus"];
if ((_x select 0) == _payGrade) then {
_paymentToDo pushBack [_player, _payGradeBonus];
_totalPayment = _totalPayment + _payGradeBonus;
};
} forEach _payGrades;
} count playableUnits;
if (_totalPayment > _orgFunds) exitWith { ["Not enough money in the organization's account!", "warning", 3] call EFUNC(misc,notify) };
{
_x params ["_player", "_bonus"];
private _bank = GETVAR(_player,FORGE_Bank,0);
private _newBalance = _bank + _bonus;
SETPVAR(_player,FORGE_Bank,_newBalance);
} count _paymentToDo;
["deduct", _totalPayment] call FUNC(adminRefresh);
_store call ["updateFunds", -_totalPayment];
};
};
ctrlSetText [2023005, ""];

View File

@ -0,0 +1,40 @@
#include "..\script_component.hpp"
/*
* Function: forge_client_admin_fnc_handleTransfer
* Author: IDSolutions
*
* [Description]
* Handles fund transfers through the admin interface.
* This function retrieves the selected player's UID and amount
* from the admin dialog, then sends it to the server-side admin store.
* Supports multiple transfer types: advance (to single player),
* deduct (from single player), advanceAll (to all players),
* and payday (distribute based on paygrade).
*
* Arguments:
* 0: Condition <STRING> - The type of transfer to perform ("advance", "deduct", "advanceAll", "payday")
*
* Return Value:
* None
*
* Examples:
* ["advance"] call forge_client_admin_fnc_handleTransfer;
* ["payday"] call forge_client_admin_fnc_handleTransfer;
*
* Public: No - Called from admin dialog controls
*/
params [["_condition", "", [""]]];
private _dialog = findDisplay 202303;
private _list = _dialog displayCtrl 2023001;
private _index = lbCurSel _list;
private _uid = _list lbData _index;
private _amount = round (parseNumber (ctrlText 2023005));
if (_condition in ["advance", "deduct"] && ((isNil "_uid") || { _uid isEqualTo "" })) exitWith { hint "You did not select a player!"; };
["forge_server_admin_handleEvents", ["handleTransfer", [_condition, _amount, _uid]]] call CFUNC(serverEvent);
ctrlSetText [2023005, ""];

View File

@ -19,33 +19,47 @@
* Public: Yes
*/
disableSerialization;
createDialog "RscAdmin";
private _dialog = findDisplay 202303;
private _list = _dialog displayCtrl 2023001;
private _list2 = _dialog displayCtrl 2023003;
{
if (str (side _x) == str (playerSide)) then {
private _name = name (_x);
private _defaultPaygrade = "E1";
private _payGrade = GETVAR(_x,FORGE_PayGrade,_defaultPaygrade);
private _index = _list lbAdd format["%1 - %2", _name, _payGrade];
_list lbSetData [_index, _name];
};
} count (allPlayers);
lbSetCurSel [2023001, 0];
private _productVersion = productVersion;
private _steamBranchName = _productVersion select 8;
private _payGrades = (missionConfigFile >> "CfgPaygrades" >> "payGrades") call BFUNC(getCfgData);
{
if (_steamBranchName == "profiling") then {
private _display = (findDisplay 46) createDisplay "RscWebAdmin";
private _ctrl = _display displayCtrl 2025;
_ctrl ctrlAddEventHandler ["JSDialog", {
params ["_control", "_isConfirmDialog", "_message"];
[QGVAR(handleEvents), [_control, _isConfirmDialog, _message]] call CFUNC(localEvent);
}];
_ctrl ctrlWebBrowserAction ["LoadFile", QUOTE(PATHTOF(ui\_site\index.html))];
} else {
disableSerialization;
createDialog "RscAdmin";
private _dialog = findDisplay 202303;
private _list = _dialog displayCtrl 2023001;
private _list2 = _dialog displayCtrl 2023003;
{
if (str (side _x) == str (playerSide) && isPlayer _x) then {
private _name = name (_x);
private _uid = getPlayerUID _x;
private _payGrade = GETVAR(_x,FORGE_PayGrade,QUOTE(E1));
private _index = _list lbAdd format["%1 - %2", _name, _payGrade];
_list lbSetData [_index, _uid];
};
} count (allPlayers);
lbSetCurSel [2023001, 0];
{
private _index = _list2 lbAdd format ["%1 - $%2", (_x select 0), ((_x select 1) call EFUNC(misc,formatNumber))];
_list2 lbSetData [_index, str _x];
} forEach _payGrades;
} forEach _payGrades;
lbSetCurSel [2023003, 0];
ctrlSetText [2023002, format ["$%1", (companyFunds call EFUNC(misc,formatNumber))]];
lbSetCurSel [2023003, 0];
ctrlSetText [2023002, format ["$%1", (companyFunds call EFUNC(misc,formatNumber))]];
};

View File

@ -1,22 +0,0 @@
#include "..\script_component.hpp"
/*
* Function: forge_client_admin_fnc_printAddonName
* Author: IDSolutions
*
* [Description]
* Prints the name of the addon to the system chat.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* None
*
* Public: No
*/
systemChat format ["Thank you for using the %1", 'ADDON'];

View File

@ -0,0 +1,37 @@
#include "..\script_component.hpp"
/*
* Function: forge_client_admin_fnc_sendMessage
* Author: IDSolutions
*
* [Description]
* Sends a message to a selected player through the admin interface.
* This function retrieves the selected player's UID and message content
* from the admin dialog, then sends it to the server-side admin store.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* [] call forge_client_admin_fnc_sendMessage;
*
* Public: No - Called from admin dialog controls
*/
private _dialog = findDisplay 202303;
private _list = _dialog displayCtrl 2023001;
private _control = _dialog displayCtrl 2023006;
private _index = lbCurSel _list;
private _uid = _list lbData _index;
private _message = ctrlText _control;
if ((isNil "_uid") || {_uid isEqualTo ""}) exitWith { hintSilent "You did not select a player!"; };
["forge_server_admin_handleEvents", ["sendMessage", [_uid, _message]]] call CFUNC(serverEvent);
hintSilent format ["Message sent to UID %1: %2", _uid, _message];
["dummy"] call FUNC(adminRefresh);

View File

@ -0,0 +1,37 @@
#include "..\script_component.hpp"
/*
* Function: forge_client_admin_fnc_updatePaygrade
* Author: IDSolutions
*
* [Description]
* Updates a player's paygrade in the server's admin store.
* This function retrieves the selected player's UID and the target paygrade
* from the admin dialog, then sends it to the server-side admin store.
*
* Arguments:
* None
*
* Return Value:
* None
*
* Examples:
* [] call forge_client_admin_fnc_updatePaygrade;
*
* Public: No - Called from admin dialog controls
*/
private _dialog = findDisplay 202303;
private _list = _dialog displayCtrl 2023001;
private _list2 = _dialog displayCtrl 2023003;
private _targetIndex = lbCurSel _list;
private _rankIndex = lbCurSel _list2;
private _uid = _list lbData _targetIndex;
private _rankData = call compile format ["%1", (_list2 lbData _rankIndex)];
private _paygrade = _rankData select 0;
if ((isNil "_uid") || {_uid isEqualTo ""}) exitWith { hintSilent "You did not select a player!" };
["forge_server_admin_handleEvents", ["updatePaygrade", [_uid, _paygrade]]] call CFUNC(serverEvent);
["dummy"] call FUNC(adminRefresh);

View File

@ -83,7 +83,7 @@ class RscAdmin {
class RscAdminPromote: RscButton {
idc = -1;
colorText[] = {1,1,1,1};
onButtonClick = "['promote'] call forge_client_admin_fnc_adminPromote;";
onButtonClick = "[] call forge_client_admin_fnc_updatePaygrade;";
soundClick[] = {"\A3\ui_f\data\sound\RscButton\soundClick",0.09,1};
text = "Promote";
x = "0.675 * safezoneW + safezoneX";
@ -163,7 +163,7 @@ class RscAdmin {
class RscAdminSend: RscButton {
idc = -1;
colorText[] = {1,1,1,1};
onButtonClick = "[] call forge_client_admin_fnc_adminMessage;";
onButtonClick = "[] call forge_client_admin_fnc_sendMessage;";
soundClick[] = {"\A3\ui_f\data\sound\RscButton\soundClick",0.09,1};
text = "Send Message";
x = "0.6125 * safezoneW + safezoneX";

View File

@ -0,0 +1,17 @@
class RscWebAdmin {
idd = 20250502;
fadein = 0;
fadeout = 0;
duration = 1e+011;
class controls {
class Background: RscText {
type = 106;
idc = 2025;
x = "safeZoneY * (pixelW/pixelH) * 2.975";
y = "safeZoneY + (safeZoneH * 0.05)";
w = "safeZoneW * (pixelW/pixelH) * 1.17";
h = "safeZoneH * 0.875";
};
};
};

View File

@ -1,22 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Forge Admin Panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
<!--
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
-->
<script>
Promise.all([
// Load CSS file
A3API.RequestFile("z\\forge_client\\addons\\admin\\ui\\_site\\styles.css"),
// Load JavaScript file
A3API.RequestFile("z\\forge_client\\addons\\admin\\ui\\_site\\script.js")
]).then(([css, js]) => {
// Apply CSS
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// Load and execute JavaScript
const script = document.createElement('script');
script.text = js;
document.head.appendChild(script);
// Initialize the admin interface
initializeAdmin();
});
</script>
</head>
<body>
<!--
Header Section
Contains the main title and server statistics
-->
<header>
<div class="header-content">
<h1>Admin Panel</h1>
<!-- Server Statistics Display -->
<div class="admin-stats">
<div class="stat-item">
<div class="stat-icon">👥</div>
<div class="stat-info">
<span class="stat-label">Online Players</span>
<span class="stat-value" id="playerCount">0</span>
@ -24,7 +46,6 @@
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-icon">👑</div>
<div class="stat-info">
<span class="stat-label">Staff Online</span>
<span class="stat-value" id="staffCount">0</span>
@ -34,10 +55,18 @@
</div>
</header>
<!--
Main Content Area
Contains all admin functionality sections
-->
<main class="container">
<div class="sections-grid">
<!--
Admin Action Sections
Left column with various global admin actions
-->
<div class="action-sections">
<!-- Global Actions Section -->
<!-- Global Payday Action -->
<div class="admin-section">
<h2>Global Actions</h2>
<div class="form-group">
@ -47,7 +76,7 @@
<button class="submit-btn" onclick="Payday()">Payday</button>
</div>
<!-- Give All Section -->
<!-- Give All Money Action -->
<div class="admin-section">
<h2>Give All Money</h2>
<div class="form-group">
@ -57,7 +86,7 @@
<button class="submit-btn" onclick="giveAllMoney()">Give to All</button>
</div>
<!-- Message System Section -->
<!-- Broadcast Message System -->
<div class="admin-section">
<h2>Broadcast Message</h2>
<div class="form-group">
@ -68,24 +97,37 @@
</div>
</div>
<!-- Player List Section -->
<!--
Player List Section
Right column showing all players with filtering options
-->
<div class="admin-section player-list-section">
<!-- Search and Filter Controls -->
<div class="search-bar">
<input type="text" id="playerSearch" class="search-input" placeholder="Search players...">
<div class="filter-bar">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="online">Online</button>
<button class="filter-btn" data-filter="staff">Staff</button>
<button class="filter-btn" data-filter="blufor">BLUFOR</button>
<button class="filter-btn" data-filter="opfor">OPFOR</button>
<button class="filter-btn" data-filter="independent">Independent</button>
<button class="filter-btn" data-filter="civilian">Civilian</button>
</div>
</div>
<!-- Dynamic Player List Container -->
<div class="player-list" id="playerList">
<!-- Players will be populated dynamically -->
<!-- Players will be populated dynamically via JavaScript -->
</div>
</div>
</div>
</main>
<!-- Message Modal -->
<!--
Modal Dialogs
Hidden by default, shown when specific actions are triggered
-->
<!-- Message Modal - For sending messages to individual players -->
<div id="messageModal" class="modal">
<div class="modal-content">
<div class="modal-header">
@ -100,7 +142,7 @@
</div>
</div>
<!-- Money Modal -->
<!-- Money Modal - For modifying player funds -->
<div id="moneyModal" class="modal">
<div class="modal-content">
<div class="modal-header">

View File

@ -1,60 +1,152 @@
// Simulated admin data - this would be replaced with actual game data
/**
* Admin Panel Management Script
* This script handles the admin panel functionality for the Arma 3 game interface.
* It provides player management, money operations, messaging, and other admin functions.
*/
//=============================================================================
// #region DATA STRUCTURES AND VARIABLES
//=============================================================================
/**
* Admin data structure - will be populated from the game
* Contains player information and payday amount configuration
*/
let adminData = {
players: [
{
id: 1,
name: "John_Doe",
rank: 5,
money: 50000,
status: "online"
},
{
id: 2,
name: "Jane_Smith",
rank: 3,
money: 25000,
status: "online"
},
{
id: 3,
name: "Mike_Johnson",
rank: 1,
money: 10000,
status: "offline"
}
],
paydayAmounts: {
1: 1000, // Rank 1 (Player) payday amount
2: 2000, // Rank 2 payday amount
3: 3000, // Rank 3 payday amount
4: 4000, // Rank 4 payday amount
5: 5000 // Rank 5 (Admin) payday amount
},
maxRank: 5
players: [], // List of all players with their details
paydayAmounts: {} // Map of paygrade to bonus amount
};
/**
* Currently selected player ID for operations that require a player selection
* @type {string|null}
*/
let selectedPlayerId = null;
// Initialize the admin panel
// #endregion
//=============================================================================
// #region INITIALIZATION AND DATA REQUESTS
//=============================================================================
/**
* Initialize the admin panel
* Sets up the UI, requests initial data from the game engine
*/
function initializeAdmin() {
updateStats();
setupFilterListeners();
requestPlayerData();
requestPaygradeData();
}
/**
* Request player data from the game engine
* Sends an event to fetch current player information
*/
function requestPlayerData() {
const message = {
event: "REQUEST_PLAYER_DATA",
data: {}
};
// Send request to the game engine
A3API.SendAlert(JSON.stringify(message));
}
/**
* Request paygrade data from the game engine
* Sends an event to fetch current paygrade configuration
*/
function requestPaygradeData() {
const message = {
event: "REQUEST_PAYGRADE_DATA",
data: {}
};
// Send request to the game engine
A3API.SendAlert(JSON.stringify(message));
}
/**
* Set up a timer to periodically refresh player data
* Ensures the admin panel shows up-to-date information
*/
function setupRefreshTimer() {
setInterval(requestPlayerData, 30000); // Refresh every 30 seconds
}
// #endregion
//=============================================================================
// #region DATA HANDLERS
//=============================================================================
/**
* Handle paygrade data received from the game engine
* Processes the paygrade list and updates the UI accordingly
*
* @param {Array} paygradeList - List of paygrade objects with paygrade and bonus properties
*/
function handlePaygradeDataRequest(paygradeList) {
try {
// Convert the paygrade list to a map for easier lookup
const paygradeMap = {};
paygradeList.forEach(item => {
paygradeMap[item.paygrade] = item.bonus;
});
adminData.paydayAmounts = paygradeMap;
// Update the player list if we already have player data
if (adminData.players.length > 0) {
updatePlayerList();
}
console.log("Paygrade data updated successfully");
} catch (error) {
console.error("Error updating paygrade data:", error);
}
}
/**
* Handle player data received from the game engine
* Updates the admin panel with current player information
*
* @param {Array} playerList - List of player objects with their details
*/
function handlePlayerDataRequest(playerList) {
adminData.players = playerList;
updateStats();
updatePlayerList();
}
// Update header statistics
// #endregion
//=============================================================================
// #region UI UPDATES AND DISPLAY
//=============================================================================
/**
* Update header statistics
* Shows counts of online players and staff
*/
function updateStats() {
const onlinePlayers = adminData.players.filter(p => p.status === "online").length;
const onlineStaff = adminData.players.filter(p => p.status === "online" && p.rank > 1).length;
const onlinePlayers = adminData.players.length;
const onlineStaff = adminData.players.filter(p => p.paygrade !== "E1").length;
document.getElementById('playerCount').textContent = onlinePlayers;
document.getElementById('staffCount').textContent = onlineStaff;
}
// Set up filter button listeners
/**
* Set up filter button listeners
* Configures the filter buttons and search functionality
*/
function setupFilterListeners() {
const filterButtons = document.querySelectorAll('.filter-btn');
// Set up filter button click handlers
filterButtons.forEach(button => {
button.addEventListener('click', () => {
filterButtons.forEach(btn => btn.classList.remove('active'));
@ -71,15 +163,26 @@ function setupFilterListeners() {
});
}
// Filter players based on category and search term
/**
* Filter players based on category and search term
*
* @param {string} filter - The filter category (all, staff, blufor, etc.)
* @param {string} searchTerm - Optional search term to filter by name
*/
function filterPlayers(filter, searchTerm = '') {
let filteredPlayers = adminData.players;
// Apply category filter
if (filter === 'online') {
filteredPlayers = filteredPlayers.filter(p => p.status === 'online');
} else if (filter === 'staff') {
filteredPlayers = filteredPlayers.filter(p => p.rank > 1);
if (filter === 'staff') {
filteredPlayers = filteredPlayers.filter(p => p.paygrade !== "E1");
} else if (filter === 'blufor') {
filteredPlayers = filteredPlayers.filter(p => p.side === "WEST");
} else if (filter === 'opfor') {
filteredPlayers = filteredPlayers.filter(p => p.side === "EAST");
} else if (filter === 'independent') {
filteredPlayers = filteredPlayers.filter(p => p.side === "GUER");
} else if (filter === 'civilian') {
filteredPlayers = filteredPlayers.filter(p => p.side === "CIV");
}
// Apply search filter
@ -93,61 +196,127 @@ function filterPlayers(filter, searchTerm = '') {
updatePlayerList(filteredPlayers);
}
// Update the player list display
/**
* Update the player list display
* Renders the filtered player list with all relevant information
*
* @param {Array} players - List of player objects to display, defaults to all players
*/
function updatePlayerList(players = adminData.players) {
const playerList = document.getElementById('playerList');
playerList.innerHTML = players.map(player => {
const paydayAmount = adminData.paydayAmounts[player.rank] || 1000; // Default to 1000 if rank not found
const paydayAmount = adminData.paydayAmounts[player.paygrade] || 1000;
const rankClass = getRankClass(player.paygrade);
return `
<div class="player-item" data-id="${player.id}">
<div class="player-item" data-id="${player.uid}">
<div class="player-info">
<span class="player-name">${player.name}</span>
<span class="player-rank rank-${player.rank}">Rank ${player.rank}</span>
<span class="player-money">$${player.money.toLocaleString()}</span>
<span class="player-payday">Payday: $${paydayAmount.toLocaleString()}</span>
<span class="player-rank rank-${rankClass}">${player.paygrade}</span>
<span class="player-money">${parseInt(player.funds).toLocaleString()}</span>
<span class="player-payday">Payday: ${paydayAmount.toLocaleString()}</span>
<span class="player-side side-${player.side.toLowerCase()}">${player.side}</span>
</div>
<div class="player-actions">
${player.rank < adminData.maxRank ? `
<button class="action-btn promote-btn" onclick="promotePlayer(${player.id})">
<button class="action-btn promote-btn" onclick="updatePaygrade('${player.uid}', true)">
Promote
</button>
` : ''}
${player.rank > 1 ? `
<button class="action-btn demote-btn" onclick="demotePlayer(${player.id})">
<button class="action-btn demote-btn" onclick="updatePaygrade('${player.uid}', false)">
Demote
</button>
` : ''}
<button class="action-btn message-btn" onclick="openMessageModal(${player.id})">Message</button>
<button class="action-btn" onclick="openMoneyModal(${player.id})">Modify Money</button>
<button class="action-btn message-btn" onclick="openMessageModal('${player.uid}', '${player.name}')">Message</button>
<button class="action-btn" onclick="openMoneyModal('${player.uid}')">Modify Money</button>
</div>
</div>
`}).join('');
}
// Rank management functions
function promotePlayer(playerId) {
const player = adminData.players.find(p => p.id === playerId);
if (player && player.rank < adminData.maxRank) {
player.rank++;
updatePlayerList();
/**
* Helper function to determine rank class based on paygrade
* Used for styling different ranks with appropriate CSS classes
*
* @param {string} paygrade - The player's paygrade code
* @returns {string} CSS class name for the rank
*/
function getRankClass(paygrade) {
if (paygrade.startsWith('E')) {
return 'enlisted';
} else if (paygrade.startsWith('WO')) {
return 'warrant';
} else if (paygrade.startsWith('O') ||
paygrade.startsWith('1') ||
paygrade.startsWith('2') ||
paygrade.startsWith('C') ||
paygrade.startsWith('M')) {
return 'officer';
} else {
return 'enlisted'; // Default
}
}
function demotePlayer(playerId) {
const player = adminData.players.find(p => p.id === playerId);
if (player && player.rank > 1) {
player.rank--;
updatePlayerList();
// #endregion
//=============================================================================
// #region RANK MANAGEMENT
//=============================================================================
/**
* Update a player's paygrade (promote or demote)
*
* @param {string} uid - Player's unique identifier
* @param {boolean} isPromotion - True for promotion, false for demotion
*/
function updatePaygrade(uid, isPromotion) {
const player = adminData.players.find(p => p.uid === uid);
if (!player) return;
// Use the paygrades from the configuration
const paygrades = Object.keys(adminData.paydayAmounts);
paygrades.sort((a, b) => adminData.paydayAmounts[a] - adminData.paydayAmounts[b]); // Sort by payment amount
const currentIndex = paygrades.indexOf(player.paygrade);
let newPaygrade;
if (isPromotion && currentIndex < paygrades.length - 1) {
newPaygrade = paygrades[currentIndex + 1];
} else if (!isPromotion && currentIndex > 0) {
newPaygrade = paygrades[currentIndex - 1];
} else {
return; // Can't promote/demote further
}
const message = {
event: "UPDATE_PAYGRADE",
data: [uid, newPaygrade]
};
A3API.SendAlert(JSON.stringify(message));
// Optimistic update
player.paygrade = newPaygrade;
updatePlayerList();
}
// Money management functions
function openMoneyModal(playerId) {
selectedPlayerId = playerId;
// #endregion
//=============================================================================
// #region MONEY MANAGEMENT
//=============================================================================
/**
* Open the money modification modal for a player
*
* @param {string} uid - Player's unique identifier
*/
function openMoneyModal(uid) {
selectedPlayerId = uid;
const modal = document.getElementById('moneyModal');
modal.style.display = 'block';
}
/**
* Close the money modification modal
*/
function closeMoneyModal() {
const modal = document.getElementById('moneyModal');
modal.style.display = 'none';
@ -155,39 +324,93 @@ function closeMoneyModal() {
selectedPlayerId = null;
}
/**
* Give money to the selected player
*/
function giveMoney() {
const amount = parseInt(document.getElementById('moneyAmount').value);
if (amount && selectedPlayerId) {
const player = adminData.players.find(p => p.id === selectedPlayerId);
if (player) {
player.money += amount;
updatePlayerList();
handleTransferFunds("advance", amount, selectedPlayerId);
closeMoneyModal();
}
}
}
/**
* Give money to all players
*/
function giveAllMoney() {
const amount = parseInt(document.getElementById('giveAllAmount').value);
const message = {
event: "ADVANCE_ALL",
data: [amount]
}
A3API.SendAlert(JSON.stringify(message));
// Request updated player data after giving money to all players
setTimeout(requestPlayerData, 500); // Short delay to allow server processing
}
/**
* Take money from the selected player
*/
function takeMoney() {
const amount = parseInt(document.getElementById('moneyAmount').value);
if (amount && selectedPlayerId) {
const player = adminData.players.find(p => p.id === selectedPlayerId);
if (player) {
player.money = Math.max(0, player.money - amount);
updatePlayerList();
handleTransferFunds("deduct", amount, selectedPlayerId);
closeMoneyModal();
}
}
}
// Message system functions
function openMessageModal(playerId) {
selectedPlayerId = playerId;
const player = adminData.players.find(p => p.id === playerId);
/**
* Handle funds transfer for a player
*
* @param {string} condition - "advance" to give money, "deduct" to take money
* @param {number} amount - Amount of money to transfer
* @param {string} uid - Player's unique identifier
*/
function handleTransferFunds(condition, amount, uid) {
const message = {
event: "HANDLE_TRANSFER",
data: [condition, amount, uid]
};
A3API.SendAlert(JSON.stringify(message));
// Optimistic update
const player = adminData.players.find(p => p.uid === uid);
if (player) {
if (condition === "advance") {
player.funds = parseInt(player.funds) + amount;
} else if (condition === "deduct") {
player.funds = Math.max(0, parseInt(player.funds) - amount);
}
updatePlayerList();
}
}
// #endregion
//=============================================================================
// #region MESSAGE SYSTEM
//=============================================================================
/**
* Open the message modal for a player
*
* @param {string} uid - Player's unique identifier
* @param {string} playerName - Player's name for display
*/
function openMessageModal(uid, playerName) {
selectedPlayerId = uid;
const modal = document.getElementById('messageModal');
document.getElementById('messagePlayerName').textContent = player.name;
document.getElementById('messagePlayerName').textContent = playerName;
modal.style.display = 'block';
}
/**
* Close the message modal
*/
function closeMessageModal() {
const modal = document.getElementById('messageModal');
modal.style.display = 'none';
@ -195,35 +418,71 @@ function closeMessageModal() {
selectedPlayerId = null;
}
/**
* Send a message to the selected player
*/
function sendPlayerMessage() {
const message = document.getElementById('messageInput').value;
if (message && selectedPlayerId) {
const player = adminData.players.find(p => p.id === selectedPlayerId);
if (player) {
console.log(`Message sent to ${player.name}: ${message}`);
const messageData = {
event: "SEND_MESSAGE",
data: [selectedPlayerId, message]
};
A3API.SendAlert(JSON.stringify(messageData));
closeMessageModal();
}
}
}
/**
* Broadcast a message to all players
*/
function broadcastMessage() {
const message = document.getElementById('broadcastMessage').value;
if (message) {
console.log(`Broadcasting message to all players: ${message}`);
const messageData = {
event: "BROADCAST_MESSAGE",
data: ["", message]
};
A3API.SendAlert(JSON.stringify(messageData));
document.getElementById('broadcastMessage').value = '';
}
}
// Global actions
// #endregion
//=============================================================================
// #region GLOBAL ACTIONS
//=============================================================================
/**
* Trigger a payday for all players
*/
function Payday() {
const amount = parseInt(document.getElementById('paydayAmount').value);
if (amount) {
adminData.players.forEach(player => {
player.money += amount;
});
updatePlayerList();
}
const message = {
event: "HANDLE_PAYDAY",
data: []
};
A3API.SendAlert(JSON.stringify(message));
// Request updated player data after payday
setTimeout(requestPlayerData, 500); // Short delay to allow server processing
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', initializeAdmin);
// #endregion
//=============================================================================
// #region EVENT LISTENERS
//=============================================================================
/**
* Initialize when DOM is loaded
*/
document.addEventListener('DOMContentLoaded', () => {
initializeAdmin();
setupRefreshTimer();
});
// #endregion

View File

@ -1,30 +1,46 @@
/* =============================================================================
BASE STYLES AND VARIABLES
============================================================================= */
/* Reset styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Color variables and theme configuration */
:root {
/* Primary colors */
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--secondary-color: #1e293b;
/* Background colors */
--background-color: #f1f5f9;
--card-background: #ffffff;
--header-bg: #1e293b;
--tile-hover: #f8fafc;
/* Text colors */
--text-primary: #0f172a;
--text-secondary: #475569;
--border-color: #e2e8f0;
--header-text: #f8fafc;
/* Status colors */
--success-color: #16a34a;
--success-hover: #15803d;
--error-color: #dc2626;
--error-hover: #b91c1c;
--warning-color: #f59e0b;
--warning-hover: #d97706;
--header-bg: #1e293b;
--header-text: #f8fafc;
--tile-hover: #f8fafc;
/* Utility colors */
--border-color: #e2e8f0;
--shadow-color: rgba(0, 0, 0, 0.1);
}
/* Base body styles */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
@ -32,6 +48,11 @@ body {
color: var(--text-primary);
}
/* =============================================================================
LAYOUT COMPONENTS
============================================================================= */
/* Main container */
.container {
max-width: 1280px;
margin: 0 auto;
@ -39,28 +60,30 @@ body {
margin-bottom: 1.5rem;
}
/* Header styles */
header {
background-color: var(--header-bg);
color: var(--header-text);
padding: 1rem 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.header-content {
& .header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1280px;
margin: 0 auto;
padding: 0 1rem;
}
}
header h1 {
& h1 {
font-size: 1.75rem;
font-weight: 600;
letter-spacing: -0.025em;
}
}
/* Admin stats in header */
.admin-stats {
display: flex;
align-items: center;
@ -68,9 +91,8 @@ header h1 {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-item {
& .stat-item {
display: flex;
align-items: center;
gap: 0.75rem;
@ -78,48 +100,50 @@ header h1 {
border-radius: 6px;
transition: all 0.2s ease-in-out;
min-width: 140px;
}
.stat-item:hover {
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
}
.stat-icon {
& .stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 1rem;
}
}
.stat-info {
& .stat-info {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
}
.stat-label {
& .stat-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.025em;
}
}
.stat-value {
& .stat-value {
font-size: 0.875rem;
font-weight: 600;
color: var(--header-text);
}
}
.stat-divider {
& .stat-divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
margin: 0 0.25rem;
}
}
/* Grid layout for sections */
.sections-grid {
display: grid;
grid-template-columns: 1fr;
@ -133,6 +157,11 @@ header h1 {
margin-top: 1.5rem;
}
/* =============================================================================
ADMIN SECTION COMPONENTS
============================================================================= */
/* Admin section cards */
.admin-section {
background-color: var(--card-background);
border-radius: 12px;
@ -146,22 +175,23 @@ header h1 {
transition: all 0.3s ease-in-out;
height: auto;
max-height: calc(100vw / 3);
}
.admin-section:hover {
&:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-4px);
}
}
.admin-section:active {
&:active {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
.admin-section.square-ratio {
&.square-ratio {
aspect-ratio: 1 / 1;
}
}
/* Player list section */
.player-list-section {
grid-column: span 1;
height: auto;
@ -174,27 +204,28 @@ header h1 {
flex: 1;
padding-right: 0.5rem;
margin-right: -0.5rem;
}
/* Customize scrollbar for webkit browsers */
.player-list::-webkit-scrollbar {
/* Customize scrollbar for webkit browsers */
&::-webkit-scrollbar {
width: 6px;
}
}
.player-list::-webkit-scrollbar-track {
&::-webkit-scrollbar-track {
background: transparent;
margin: 0.5rem;
}
}
.player-list::-webkit-scrollbar-thumb {
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.1);
border-radius: 3px;
}
.player-list::-webkit-scrollbar-thumb:hover {
&:hover {
background-color: rgba(0, 0, 0, 0.2);
}
}
}
/* Player item in the list */
.player-item {
display: flex;
justify-content: space-between;
@ -206,30 +237,42 @@ header h1 {
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
margin-bottom: 0.5rem;
}
.player-item:last-child {
&:last-child {
margin-bottom: 0;
}
}
.player-item:hover {
&:hover {
background-color: var(--tile-hover);
box-shadow: 0 4px 6px var(--shadow-color);
transform: translateY(-2px);
}
}
.player-info {
& .player-info {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
}
.player-name {
& .player-name {
font-weight: 600;
color: var(--text-primary);
}
& .player-money {
font-size: 0.875rem;
color: var(--success-color);
font-weight: 500;
}
& .player-actions {
display: flex;
gap: 0.5rem;
}
}
/* Player role badges */
.player-role {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
@ -254,17 +297,11 @@ header h1 {
color: white;
}
.player-money {
font-size: 0.875rem;
color: var(--success-color);
font-weight: 500;
}
.player-actions {
display: flex;
gap: 0.5rem;
}
/* =============================================================================
BUTTONS AND INTERACTIVE ELEMENTS
============================================================================= */
/* Action buttons */
.action-btn {
padding: 0.5rem 1rem;
border-radius: 6px;
@ -278,30 +315,31 @@ header h1 {
.promote-btn {
background-color: #22c55e;
color: white;
}
.promote-btn:hover {
&:hover {
background-color: #16a34a;
}
}
.demote-btn {
background-color: #ef4444;
color: white;
}
.demote-btn:hover {
&:hover {
background-color: #dc2626;
}
}
.message-btn {
background-color: #3b82f6;
color: white;
}
.message-btn:hover {
&:hover {
background-color: #2563eb;
}
}
/* Search and filter components */
.search-bar {
display: flex;
flex-direction: column;
@ -317,32 +355,63 @@ header h1 {
color: var(--text-primary);
background-color: var(--card-background);
transition: all 0.2s ease-in-out;
}
.search-input:focus {
&:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
&:hover {
border-color: var(--primary-color);
}
}
.search-input:hover {
border-color: var(--primary-color);
.filter-bar {
display: flex;
gap: 0.5rem;
}
.filter-btn {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
background-color: var(--card-background);
color: var(--text-secondary);
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s ease-in-out;
&:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
&.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
}
/* =============================================================================
FORMS AND INPUTS
============================================================================= */
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.form-group label {
& label {
font-weight: 500;
color: var(--text-secondary);
font-size: 0.875rem;
}
}
.form-group input {
& input {
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
@ -350,16 +419,17 @@ header h1 {
color: var(--text-primary);
background-color: var(--card-background);
transition: all 0.2s ease-in-out;
}
.form-group input:focus {
&:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
}
.form-group input:hover {
&:hover {
border-color: var(--primary-color);
}
}
}
.submit-btn {
@ -374,21 +444,25 @@ header h1 {
transition: all 0.2s ease-in-out;
opacity: 0.9;
margin-top: auto;
}
.submit-btn:hover {
&:hover {
background-color: var(--primary-hover);
opacity: 1;
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
}
.submit-btn:active {
&:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
.modal {
/* =============================================================================
MODALS
============================================================================= */
.modal {
display: none;
position: fixed;
top: 0;
@ -417,12 +491,12 @@ header h1 {
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-header h2 {
& h2 {
font-size: 1.25rem;
color: var(--text-primary);
font-weight: 600;
}
}
.close-modal {
@ -432,11 +506,15 @@ header h1 {
cursor: pointer;
color: var(--text-secondary);
transition: color 0.2s ease-in-out;
&:hover {
color: var(--text-primary);
}
}
.close-modal:hover {
color: var(--text-primary);
}
/* =============================================================================
BADGES AND STATUS INDICATORS
============================================================================= */
.badge {
padding: 0.25rem 0.5rem;
@ -447,34 +525,6 @@ header h1 {
letter-spacing: 0.025em;
}
.filter-bar {
display: flex;
gap: 0.5rem;
}
.filter-btn {
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
background-color: var(--card-background);
color: var(--text-secondary);
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s ease-in-out;
}
.filter-btn:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
.filter-btn.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
/* Rank badges */
.player-rank {
padding: 0.25rem 0.5rem;
@ -485,37 +535,44 @@ header h1 {
letter-spacing: 0.025em;
}
.rank-1 {
/* Rank styling based on type */
.rank-enlisted {
background-color: #e2e8f0;
color: #475569;
}
.rank-2 {
.rank-warrant {
background-color: #bfdbfe;
color: #1e40af;
}
.rank-3 {
background-color: #93c5fd;
color: #1e40af;
}
.rank-4 {
background-color: #60a5fa;
color: #1e40af;
}
.rank-5 {
.rank-officer {
background-color: #3b82f6;
color: #ffffff;
}
.payday-description {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0.5rem 0;
/* Side indicators */
.side-west {
background-color: #3b82f6;
color: white;
}
.side-east {
background-color: #ef4444;
color: white;
}
.side-guer {
background-color: #22c55e;
color: white;
}
.side-civ {
background-color: #a855f7;
color: white;
}
/* Player payday indicator */
.player-payday {
font-size: 0.75rem;
color: var(--success-color);
@ -525,3 +582,64 @@ header h1 {
border-radius: 4px;
margin-left: 0.5rem;
}
/* Description text */
.payday-description {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0.5rem 0;
}
/* =============================================================================
RESPONSIVE ADJUSTMENTS
============================================================================= */
/* Adjustments for smaller screens */
@media (max-width: 768px) {
.action-sections {
grid-template-columns: 1fr;
}
.player-item {
flex-direction: column;
align-items: flex-start;
& .player-info {
margin-bottom: 1rem;
flex-wrap: wrap;
}
& .player-actions {
width: 100%;
justify-content: space-between;
}
}
.filter-bar {
overflow-x: auto;
padding-bottom: 0.5rem;
}
.modal-content {
min-width: 90%;
max-width: 90%;
}
}
/* Adjustments for very small screens */
@media (max-width: 480px) {
.admin-stats {
flex-direction: column;
& .stat-divider {
width: 80%;
height: 1px;
margin: 0.25rem 0;
}
}
.action-btn {
padding: 0.5rem;
font-size: 0.75rem;
}
}

View File

@ -26,15 +26,15 @@ private _default = [[],[],[],[]];
private _armory_unlocks = GETVAR(player,Armory_Unlocks,_default);
private _typeToNumber = switch (_type) do {
case "backpack": {3};
case "facewear": {0};
case "headgear": {0};
case "hmd": {0};
case "item": {0};
case "magazine": {2};
case "uniform": {0};
case "facewear";
case "headgear";
case "hmd";
case "item";
case "uniform";
case "vest": {0};
case "weapon": {1};
case "magazine": {2};
case "backpack": {3};
default {0};
};

View File

@ -5,25 +5,26 @@
* Author: IDSolutions
*
* [Description]
* Initializes the arsenal system
* Initializes the arsenal system with armory and garage data
*
* Arguments:
* 0: Armory data <ARRAY>
* 1: Garage data <ARRAY>
* 0: Armory data <ARRAY> - [items, weapons, magazines, backpacks]
* 1: Garage data <ARRAY> - [cars, armor, helicopters, planes, naval, static]
*
* Return Value:
* None
* BOOLEAN - true if initialization successful, false if invalid data
*
* Examples:
* None
*
* Public: Yes
* Example:
* [[_items, _weapons, _magazines, _backpacks], [_cars, _armor, _helis, _planes, _naval, _statics]] call forge_client_arsenal_fnc_initArsenal
*/
params [["_armory_data", [], [[]]], ["_garage_data", [], [[]]]];
if (count _armory_data isEqualTo [""]) then { _armory_data = [[],[],[],[]] };
if (count _garage_data isEqualTo [""]) then { _garage_data = [[],[],[],[],[],[]] };
private _defaultArmory = [[],[],[],[]];
private _defaultGarage = [[],[],[],[],[],[]];
if (!(_armory_data isEqualTypeArray _defaultArmory) || (count _armory_data != 4)) then { _armory_data = _defaultArmory; };
if (!(_garage_data isEqualTypeArray _defaultGarage) || (count _garage_data != 6)) then { _garage_data = _defaultGarage; };
if (GVAR(armory_type) == 0) then {
{
[GVAR(gear_box), _x, false, true, 1, _forEachIndex] call BFUNC(addVirtualItemCargo);
@ -34,23 +35,29 @@ if (GVAR(armory_type) == 0) then {
} forEach _armory_data;
};
_armory_data params [["_items", [], [[]]], ["_weapons", [], [[]]], ["_magazines", [], [[]]], ["_backpacks", [], [[]]]];
_garage_data params [["_cars", [], [[]]], ["_armor", [], [[]]], ["_helis", [], [[]]], ["_planes", [], [[]]], ["_naval", [], [[]]], ["_statics", [], [[]]]];
GVAR(armory_unlocks) = _armory_data;
GVAR(garage_unlocks) = _garage_data;
GVAR(item_unlocks) = _armory_data select 0;
GVAR(weapon_unlocks) = _armory_data select 1;
GVAR(magazine_unlocks) = _armory_data select 2;
GVAR(backpack_unlocks) = _armory_data select 3;
GVAR(item_unlocks) = _items;
GVAR(weapon_unlocks) = _weapons;
GVAR(magazine_unlocks) = _magazines;
GVAR(backpack_unlocks) = _backpacks;
GVAR(car_unlocks) = _garage_data select 0;
GVAR(armor_unlocks) = _garage_data select 1;
GVAR(heli_unlocks) = _garage_data select 2;
GVAR(plane_unlocks) = _garage_data select 3;
GVAR(naval_unlocks) = _garage_data select 4;
GVAR(static_unlocks) = _garage_data select 5;
GVAR(car_unlocks) = _cars;
GVAR(armor_unlocks) = _armor;
GVAR(heli_unlocks) = _helis;
GVAR(plane_unlocks) = _planes;
GVAR(naval_unlocks) = _naval;
GVAR(static_unlocks) = _statics;
{
[_x] call FUNC(addVirtualVehicles);
} forEach GVAR(garage_unlocks);
TRACE_2("Arsenal System Initialized with defaults",count GVAR(armory_unlocks),count GVAR(garage_unlocks));
private _armoryCount = count (_armory_data select { count _x > 0 });
private _garageCount = count (_garage_data select { count _x > 0 });
TRACE_2("Arsenal System Initialized",_armoryCount,_garageCount);

View File

@ -31,10 +31,10 @@ getPlayerUID player,
"garage", [GETVAR(player,FORGE_Garage,[])],
"cash", [GETVAR(player,FORGE_Cash,0)],
"bank", [GETVAR(player,FORGE_Bank,0)],
"number", [GETVAR(player,FORGE_Phone_Number,"unknown")],
"email", [GETVAR(player,FORGE_Email,"unknown@spearnet.mil")],
"paygrade", [GETVAR(player,FORGE_PayGrade,"E1")],
"organization", [GETVAR(player,FORGE_Organization,"")],
"number", [GETVAR(player,FORGE_Phone_Number,QUOTE(unknown))],
"email", [GETVAR(player,FORGE_Email,QUOTE(unknown@spearnet.mil))],
"paygrade", [GETVAR(player,FORGE_PayGrade,QUOTE(E1))],
"organization", [GETVAR(player,FORGE_Organization,QUOTE(None))],
"reputation", [rating player],
"loadout", [getUnitLoadout player],
"holster", [GETVAR(player,FORGE_Holster_Weapon,true)],

View File

@ -2,6 +2,7 @@ PREP(buyItem);
PREP(buyVehicle);
PREP(changeFilter);
PREP(changePayment);
PREP(handleDelivery);
PREP(handlePurchase);
PREP(initStore);
PREP(openStore);

View File

@ -30,10 +30,7 @@ private _locker = GETVAR(player,FORGE_Locker,[]);
if !([_price] call FUNC(handlePurchase)) exitWith {};
switch (_configType) do {
case "item": {
_displayName = getText (configFile >> "CfgWeapons" >> _className >> "displayName");
_locker pushBack [_itemType, _className];
};
case "item";
case "weapon": {
_displayName = getText (configFile >> "CfgWeapons" >> _className >> "displayName");
_locker pushBack [_itemType, _className];
@ -48,7 +45,7 @@ switch (_configType) do {
};
};
SETPVAR(player,FORGE_Locker,_locker);
[_locker] spawn FUNC(handleDelivery);
[_className, _itemType] call EFUNC(arsenal,addArmoryItem);

View File

@ -38,14 +38,7 @@ private _items = _data select 1;
if (_category == _selectedCategory) then {
switch (_configType) do {
case "item": {
private _displayName = getText (configFile >> "CfgWeapons" >> _item >> "displayName");
private _picture = getText (configFile >> "CfgWeapons" >> _item >> "picture");
_index = _productList lbAdd _displayName;
_productList lbSetData [_index, str [_item, _price, _category, _configType, _itemType]];
_productList lbSetPicture [_index, _picture];
};
case "item";
case "weapon": {
private _displayName = getText (configFile >> "CfgWeapons" >> _item >> "displayName");
private _picture = getText (configFile >> "CfgWeapons" >> _item >> "picture");
@ -62,14 +55,7 @@ private _items = _data select 1;
_productList lbSetData [_index, str [_item, _price, _category, _configType, _itemType]];
_productList lbSetPicture [_index, _picture];
};
case "backpack": {
private _displayName = getText (configFile >> "CfgVehicles" >> _item >> "displayName");
private _picture = getText (configFile >> "CfgVehicles" >> _item >> "picture");
_index = _productList lbAdd _displayName;
_productList lbSetData [_index, str [_item, _price, _category, _configType, _itemType]];
_productList lbSetPicture [_index, _picture];
};
case "backpack";
case "vehicle": {
private _displayName = getText (configFile >> "CfgVehicles" >> _item >> "displayName");
private _picture = getText (configFile >> "CfgVehicles" >> _item >> "picture");

View File

@ -0,0 +1,66 @@
#include "..\script_component.hpp"
/*
* Function: forge_store_fnc_handleDelivery
* Description:
* Handles the delivery timer and locker updates for purchased items
*
* Parameters:
* 0: New Locker Contents <ARRAY>
*
* Returns:
* None
*
* Example:
* [_newLocker] spawn forge_store_fnc_handleDelivery
*/
params [["_newLocker", [], [[]]]];
private _deliveryTime = ["DT", 0] call BIS_fnc_getParamValue;
if (_newLocker isEqualTo []) exitWith {};
if (_deliveryTime > 0) then {
[
format [
"<t align='left'>Order Processing</t><br/><t size='0.8' align='left'>Estimated delivery: %1</t>",
[_deliveryTime, "MM:SS"] call BIS_fnc_secondsToString
],
"info",
3,
"right"
] call EFUNC(misc,notify);
uiSleep (_deliveryTime / 2);
[
"<t align='left'>Package in transit</t>",
"warning",
2,
"left"
] call EFUNC(misc,notify);
uiSleep (_deliveryTime / 2);
SETPVAR(player,FORGE_Locker,_newLocker);
[
"<t align='left'>Order Delivered!</t><br/><t size='0.8' align='left'>Check your locker</t>",
"success",
5,
"left"
] call EFUNC(misc,notify);
if (hasInterface) then { playSound "FD_Finish_F"; };
} else {
SETPVAR(player,FORGE_Locker,_newLocker);
[
"<t align='left'>Order Complete!</t><br/><t size='0.8' align='left'>Items added to locker</t>",
"success",
5,
"left"
] call EFUNC(misc,notify);
if (hasInterface) then { playSound "FD_Finish_F"; };
};

View File

@ -47,14 +47,13 @@ if (_payment select 0 == "Organization") then {
};
};
private _varType = _payment select 2;
private _varType = toLower (_payment select 2);
private _varName = _payment param [1, "", [""]];
private _balance = switch (_varType) do {
case "organization": {
_store call ["getFunds", []];
};
case "player": { player getVariable [_payment select 1, 0] };
case "mission": { missionNamespace getVariable [_payment select 1, 0] };
default { 0 };
case "organization": { _store call ["getFunds", []] };
case "player": { GETVAR(player,_varName,0) };
case "mission": { GETVAR(missionNamespace,_varName,0) };
default { diag_log "[FORGE Store] Error: Unknown payment type"; 0 };
};
if (_balance < _price) exitWith {
@ -63,14 +62,14 @@ if (_balance < _price) exitWith {
};
switch (_varType) do {
case "organization": {
_store call ["updateFunds", -_price];
};
case "organization": { _store call ["updateFunds", -_price] };
case "player": {
player setVariable [_payment select 1, (_balance - _price), true];
private _newBalance = _balance - _price;
SETPVAR(player,_varName,_newBalance);
};
case "mission": {
missionNamespace setVariable [_payment select 1, (_balance - _price), true];
private _newBalance = _balance - _price;
SETPVAR(missionNamespace,_varName,_newBalance);
};
};

View File

@ -30,14 +30,11 @@ private _display = findDisplay IDD_STOREDIALOG;
private _categoryList = _display displayCtrl IDC_CATEGORYLIST;
private _paymentList = _display displayCtrl IDC_PAYMENTLIST;
private _storeName = _display displayCtrl IDC_DIALOGNAME;
private _data = _store getVariable "storeData";
private _categories = _data select 0;
private _products = _data select 1;
private _name = _data select 2;
private _paymentMethods = _data select 3;
private _data = _store getVariable ["storeData", []];
_data params [["_categories", [], [[]]], ["_products", [], [[]]], ["_name", "", [""]], ["_paymentMethods", [], [[]]]];
GVAR(currentStore) = _data;
_storeName ctrlSetText _name;
{

View File

@ -23,12 +23,9 @@ private _display = findDisplay IDD_STOREDIALOG;
private _productList = _display displayCtrl IDC_PRODUCTLIST;
private _productIndex = lbCurSel _productList;
private _productData = lbData [IDC_PRODUCTLIST, _productIndex];
private _product = call compile _productData;
private _item = _product select 0;
private _price = _product select 1;
private _configType = _product select 3;
private _itemType = _product select 4;
_product params [["_item", "", [""]], ["_price", 0, [0]], ["_category", "", [""]], ["_configType", "", [""]], ["_itemType", "", [""]]];
switch (_configType) do {
case "item";