feat: implement complete Forge framework with Rust/Redis backend and Arma 3 integration
Implemented features: - High-performance Rust extension with Redis persistence - Actor/player management with loadout, position, and state tracking - Banking system with deposit, withdraw, and transfer operations - Physical and virtual garage/locker systems for vehicle and equipment storage - Organization management with member tracking and permissions - Client-side UI with React-like state management - Server-side event-driven architecture with CBA Events - Security: Self-transfer prevention at multiple layers - Logging system with per-module log files - ICOM module for inter-server communication Co-Authored-By: Warp <agent@warp.dev>
This commit is contained in:
parent
7ce6c0bcad
commit
ebfe77a340
@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"arma/server/extension",
|
||||
"bin/icom",
|
||||
"lib/models",
|
||||
"lib/repositories",
|
||||
"lib/services",
|
||||
|
||||
@ -271,13 +271,13 @@ Logs are automatically created in `@forge_server/logs/`:
|
||||
|
||||
## License
|
||||
|
||||
[Your License Here]
|
||||
View the License [here](LICENSE.md).
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: [Gitea Issues](https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues)
|
||||
- **Documentation**: See individual module READMEs
|
||||
- **Architecture**: [FORGE_Architecture_Diagram.md](FORGE_Architecture_Diagram.md)
|
||||
- **Architecture**: [Diagram](Architecture_Diagram.md)
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@ -3,11 +3,19 @@ workshop = [
|
||||
"450814997", # CBA_A3
|
||||
"3499977893", # Advanced Dev Tools
|
||||
"623475643", # 3DEN Enhanced
|
||||
"3023395342", # 3DEN Attributes Fast Load
|
||||
]
|
||||
presets = []
|
||||
dlc = []
|
||||
optionals = []
|
||||
parameters = []
|
||||
parameters = [
|
||||
"-skipIntro",
|
||||
"-noSplash",
|
||||
"-showScriptErrors",
|
||||
"-debug",
|
||||
"-filePatching",
|
||||
"-world=empty",
|
||||
]
|
||||
|
||||
[ace]
|
||||
extends = "default"
|
||||
|
||||
@ -11,10 +11,10 @@ git_hash = 0
|
||||
include = [
|
||||
"mod.cpp",
|
||||
"meta.cpp",
|
||||
"logo_forge_client.png",
|
||||
"logo_forge_client_over.png",
|
||||
"logo_forge_client_ca.paa",
|
||||
"logo_forge_client_over_ca.paa",
|
||||
"icon_64_ca.paa",
|
||||
"icon_128_ca.paa",
|
||||
"icon_128_highlight_ca.paa",
|
||||
"title_ca.paa",
|
||||
"LICENSE.md",
|
||||
"README.md",
|
||||
]
|
||||
|
||||
@ -8,21 +8,52 @@ removeBackpack player;
|
||||
removeGoggles player;
|
||||
removeHeadgear player;
|
||||
|
||||
SETPVAR(player,FORGE_actorIsLoaded,false);
|
||||
SETPVAR(player,FORGE_isLoaded,false);
|
||||
cutText ["Loading In...", "BLACK", 1];
|
||||
|
||||
player addEventHandler ["Killed", {
|
||||
params ["_unit", "_killer", "_instigator", "_useEffects"];
|
||||
[SRPC(economy,onKilled), [_unit]] call CFUNC(serverEvent);
|
||||
}];
|
||||
|
||||
player addEventHandler ["Respawn", {
|
||||
params ["_unit", "_corpse"];
|
||||
|
||||
private _uid = getPlayerUID player;
|
||||
[SRPC(economy,onRespawn), [_unit, _corpse, _uid]] call CFUNC(serverEvent);
|
||||
}];
|
||||
|
||||
if (isNil QGVAR(ActorClass)) then { [] call FUNC(initActorClass); };
|
||||
|
||||
[QGVAR(initActor), {
|
||||
GVAR(ActorClass) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(onActorRespawn), {
|
||||
params [["_loadout", [], [[]]], ["_medSpawnPos", [0,0,0], [[]]], ["_medSpawnDir", 0, [0]]];
|
||||
|
||||
private _message = ["warning", "Medical Alert", "You have been revived at a medical facility.", 5000];
|
||||
EGVAR(notifications,NotificationClass) call ["create", _message];
|
||||
|
||||
player setUnitLoadout _loadout;
|
||||
player setPosATL _medSpawnPos;
|
||||
player setDir _medSpawnDir;
|
||||
player switchMove "Acts_LyingWounded_loop";
|
||||
|
||||
["Initialize", [player, [], false, true, true, true, true, true, false, false]] call BFUNC(EGSpectator);
|
||||
|
||||
[SRPC(economy,onHealed), [player]] call CFUNC(serverEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(onActorHealed), {
|
||||
player switchMove "";
|
||||
["Terminate"] call BFUNC(EGSpectator);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitActor), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(ActorClass) call ["sync", [_data, true]];
|
||||
|
||||
SETPVAR(player,FORGE_isLoaded,true);
|
||||
cutText ["", "PLAIN", 1];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -35,7 +66,7 @@ if (isNil QGVAR(ActorClass)) then { [] call FUNC(initActorClass); };
|
||||
[QGVAR(initActor), []] call CFUNC(localEvent);
|
||||
|
||||
[{
|
||||
GETVAR(player,FORGE_actorIsLoaded,false)
|
||||
GETVAR(player,FORGE_isLoaded,false)
|
||||
}, {
|
||||
private _holster = GVAR(ActorClass) call ["get", ["holster", true]];
|
||||
if (_holster) then { [player] call AFUNC(weaponselect,putWeaponAway); };
|
||||
|
||||
@ -8,3 +8,18 @@ private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
#include "initSettings.inc.sqf"
|
||||
#include "initKeybinds.inc.sqf"
|
||||
|
||||
["ace_refuel_started", {
|
||||
params ["_source", "_target", "", "_unit"];
|
||||
[SRPC(economy,FuelStart), [_source, _target, _unit]] call CFUNC(serverEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
["ace_refuel_tick", {
|
||||
params ["_source", "_target", "_amount"];
|
||||
[SRPC(economy,FuelTick), [_source, _target, _amount]] call CFUNC(serverEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
["ace_refuel_stopped", {
|
||||
params ["_source", "_target"];
|
||||
[SRPC(economy,FuelStop), [_source, _target]] call CFUNC(serverEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -27,12 +27,14 @@ diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _ev
|
||||
|
||||
switch (_event) do {
|
||||
case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; };
|
||||
// case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
|
||||
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."; }; // TODO: Implement device interaction
|
||||
case "actor::open::garage": { hint "Garage interaction is not yet implemented."; }; // TODO: Implement garage interaction
|
||||
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
|
||||
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
|
||||
case "actor::open::locker": { hint "Locker interaction is not yet implemented."; }; // TODO: Implement locker interaction
|
||||
case "actor::open::vlocker": { ["Open", [false, FORGE_Locker_Box, player]] spawn BFUNC(arsenal) };
|
||||
// case "actor::open::phone": { [] spawn EFUNC(phone,openUI) };
|
||||
case "actor::open::phone": { hint "Phone interaction is not yet implemented."; }; // TODO: Implement phone interaction
|
||||
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." }; // TODO: Implement player interaction
|
||||
|
||||
@ -64,7 +64,6 @@ GVAR(ActorClass) = createHashMapObject [[
|
||||
private _actor = _self get "actor";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
if (_data isEqualTo createHashMap) exitWith {
|
||||
diag_log "[FORGE:Client:Actor] Empty data received for sync, skipping.";
|
||||
};
|
||||
@ -82,12 +81,12 @@ GVAR(ActorClass) = createHashMapObject [[
|
||||
default {};
|
||||
};
|
||||
};
|
||||
|
||||
} forEach _data;
|
||||
|
||||
_self set ["actor", _actor];
|
||||
SETPVAR(player,FORGE_isLoaded,true);
|
||||
|
||||
SETPVAR(player,FORGE_actorIsLoaded,true);
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Actor] Sync completed";
|
||||
}],
|
||||
["get", {
|
||||
|
||||
@ -14,11 +14,11 @@
|
||||
[
|
||||
QGVAR(enableVA), "CHECKBOX",
|
||||
[LSTRING(enableVA), LSTRING(enableVATooltip)],
|
||||
_category, false, true
|
||||
_category, true, true
|
||||
] call CBA_fnc_addSetting;
|
||||
|
||||
[
|
||||
QGVAR(enableVG), "CHECKBOX",
|
||||
[LSTRING(enableVG), LSTRING(enableVGTooltip)],
|
||||
_category, false, true
|
||||
_category, true, true
|
||||
] call CBA_fnc_addSetting;
|
||||
|
||||
@ -47,6 +47,13 @@ const actions = {
|
||||
//=============================================================================
|
||||
|
||||
const baseMenuItems = [
|
||||
{
|
||||
id: "atm",
|
||||
title: "ATM",
|
||||
description: "Access the ATM",
|
||||
icon: "",
|
||||
action: "actor::open::atm",
|
||||
},
|
||||
{
|
||||
id: "bank",
|
||||
title: "Banking Services",
|
||||
@ -111,7 +118,7 @@ const actionDefinitions = {
|
||||
title: "Virtual Arsenal",
|
||||
description: "Access your virtual arsenal",
|
||||
icon: "",
|
||||
action: "actor::open::arsenal",
|
||||
action: "actor::open::vlocker",
|
||||
},
|
||||
vg: {
|
||||
id: "vg",
|
||||
@ -141,7 +148,7 @@ function actorReducer(state = initialState, action) {
|
||||
actionArray.forEach((actionItem) => {
|
||||
if (Array.isArray(actionItem) && actionItem.length === 2) {
|
||||
const [type, value] = actionItem;
|
||||
const definition = state.actionDefinitions[value];
|
||||
const definition = state.actionDefinitions[type];
|
||||
if (definition) {
|
||||
newMenuItems.push(definition);
|
||||
} else {
|
||||
|
||||
@ -10,9 +10,6 @@ if (isNil QGVAR(BankClass)) then { [] call FUNC(initBankClass); };
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(BankClass) call ["sync", [_data, true]];
|
||||
|
||||
SETPVAR(player,FORGE_isLoaded,true);
|
||||
cutText ["", "PLAIN", 1];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncBank), {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"J.Schmidt"};
|
||||
authors[] = {"IDSolutions"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
|
||||
@ -23,11 +23,89 @@ private _event = _alert get "event";
|
||||
private _data = _alert get "data";
|
||||
private _display = displayChild findDisplay 46;
|
||||
|
||||
private _uid = GVAR(BankClass) get "uid";
|
||||
private _account = GVAR(BankClass) get "account";
|
||||
private _cash = _account get "cash";
|
||||
private _bank = _account get "bank";
|
||||
private _pin = _account get "pin";
|
||||
|
||||
diag_log format ["[FORGE:Client:Bank] Handling UI event: %1 with data: %2", _event, _data];
|
||||
|
||||
switch (_event) do {
|
||||
case "bank::close": { _display closeDisplay 1; };
|
||||
default { hint format ["Unhandled UI event: %1", _event]; };
|
||||
// ========================================================================
|
||||
// DATA REQUESTS
|
||||
// ========================================================================
|
||||
case "bank::sync": {
|
||||
private _org = 0; // TODO: Get org balance
|
||||
private _players = SREG(bank,NameRegistry);
|
||||
private _accountData = createHashMapFromArray [
|
||||
["uid", _uid],
|
||||
["cash", _cash],
|
||||
["bank", _bank],
|
||||
["org", _org],
|
||||
["pin", _pin],
|
||||
["players", _players]
|
||||
];
|
||||
|
||||
_control ctrlWebBrowserAction ["ExecJS", format ["syncDataFromArma(%1)", toJSON _accountData]];
|
||||
};
|
||||
|
||||
// ========================================================================
|
||||
// BANK OPERATIONS
|
||||
// ========================================================================
|
||||
case "bank::deposit": {
|
||||
private _amount = _data get "amount";
|
||||
if (_amount > _cash) exitWith { hint "Insufficient cash!"; };
|
||||
|
||||
[SRPC(bank,requestDeposit), [_uid, _amount]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "bank::withdraw": {
|
||||
private _amount = _data get "amount";
|
||||
if (_amount > _bank) exitWith { hint "Insufficient funds!"; };
|
||||
|
||||
[SRPC(bank,requestWithdraw), [_uid, _amount]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "bank::transfer": {
|
||||
private _amount = _data get "amount";
|
||||
private _from = _data get "from";
|
||||
private _target = _data get "target";
|
||||
|
||||
// Prevent self-transfers
|
||||
if (_target isEqualTo _uid) exitWith {
|
||||
hint "Cannot transfer to yourself!";
|
||||
diag_log "[FORGE:Client:Bank] Attempted self-transfer blocked";
|
||||
};
|
||||
|
||||
private _fromAmount = _account get _from;
|
||||
if (_amount > _fromAmount) exitWith { hint "Insufficient funds!"; };
|
||||
|
||||
[SRPC(bank,requestTransfer), [_uid, _target, _from, _amount]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "bank::close": {
|
||||
_display closeDisplay 1;
|
||||
};
|
||||
|
||||
// ========================================================================
|
||||
// ATM OPERATIONS
|
||||
// ========================================================================
|
||||
case "atm::withdraw": {
|
||||
private _amount = _data get "amount";
|
||||
if (_amount > _bank) exitWith { hint "Insufficient funds!"; };
|
||||
|
||||
[SRPC(bank,requestWithdraw), [_uid, _amount]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "atm::deposit": {
|
||||
private _amount = _data get "amount";
|
||||
if (_amount > _cash) exitWith { hint "Insufficient cash!"; };
|
||||
|
||||
[SRPC(bank,requestDeposit), [_uid, _amount]] call CFUNC(serverEvent);
|
||||
};
|
||||
case "atm::close": {
|
||||
_display closeDisplay 1;
|
||||
};
|
||||
default {
|
||||
diag_log format ["[FORGE:Client:Bank] Unhandled UI event: %1", _event];
|
||||
};
|
||||
};
|
||||
|
||||
true;
|
||||
|
||||
@ -25,19 +25,14 @@ GVAR(BankClass) = createHashMapObject [[
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
|
||||
private _actor = EGVAR(actor,ActorClass) get "actor";
|
||||
private _phone_number = _actor get "phone_number";
|
||||
private _email = _actor get "email";
|
||||
|
||||
private _account = createHashMap;
|
||||
_account set ["uid", (getPlayerUID player)];
|
||||
_account set ["name", (name player)];
|
||||
_account set ["bank", 0];
|
||||
_account set ["cash", 0];
|
||||
_account set ["earnings", 0];
|
||||
_account set ["pin", 1234];
|
||||
_account set ["transactions", []];
|
||||
_account set ["phone_number", _phone_number];
|
||||
_account set ["email", _email];
|
||||
|
||||
_self set ["account", _account];
|
||||
}],
|
||||
@ -64,7 +59,6 @@ GVAR(BankClass) = createHashMapObject [[
|
||||
private _account = _self get "account";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
if (_data isEqualTo createHashMap) exitWith {
|
||||
diag_log "[FORGE:Client:Bank] Empty data received for sync, skipping.";
|
||||
};
|
||||
@ -74,6 +68,8 @@ GVAR(BankClass) = createHashMapObject [[
|
||||
} forEach _data;
|
||||
|
||||
_self set ["account", _account];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Bank] Sync completed";
|
||||
}],
|
||||
["get", {
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
params [["_isATM", false, [false]]];
|
||||
|
||||
private _display = (findDisplay 46) createDisplay "RscBank";
|
||||
private _ctrl = (_display displayCtrl 1002);
|
||||
|
||||
@ -25,7 +27,11 @@ _ctrl ctrlAddEventHandler ["JSDialog", {
|
||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
||||
}];
|
||||
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
|
||||
if (_isATM) then {
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\atm.html)];
|
||||
} else {
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\bank.html)];
|
||||
};
|
||||
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
|
||||
|
||||
true;
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATM</title>
|
||||
<link rel="stylesheet" href="atm.css" />
|
||||
<!-- <script src="store.js"></script> -->
|
||||
<!-- <link rel="stylesheet" href="atm.css" /> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
@ -14,19 +15,26 @@
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\actor\\ui\\_site\\atm.css",
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\atm.css",
|
||||
),
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\actor\\ui\\_site\\atm.js",
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\store.js",
|
||||
),
|
||||
]).then(([css, js]) => {
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\atm.js",
|
||||
),
|
||||
]).then(([css, storeJs, atmJs]) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
const store = document.createElement("script");
|
||||
store.text = storeJs;
|
||||
document.head.appendChild(store);
|
||||
|
||||
const atm = document.createElement("script");
|
||||
atm.text = atmJs;
|
||||
document.head.appendChild(atm);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
@ -64,19 +72,8 @@
|
||||
<span class="pin-dot"></span>
|
||||
<span class="pin-dot"></span>
|
||||
</div>
|
||||
<div class="keypad">
|
||||
<button class="key-btn" onclick="enterPin('1')">1</button>
|
||||
<button class="key-btn" onclick="enterPin('2')">2</button>
|
||||
<button class="key-btn" onclick="enterPin('3')">3</button>
|
||||
<button class="key-btn" onclick="enterPin('4')">4</button>
|
||||
<button class="key-btn" onclick="enterPin('5')">5</button>
|
||||
<button class="key-btn" onclick="enterPin('6')">6</button>
|
||||
<button class="key-btn" onclick="enterPin('7')">7</button>
|
||||
<button class="key-btn" onclick="enterPin('8')">8</button>
|
||||
<button class="key-btn" onclick="enterPin('9')">9</button>
|
||||
<button class="key-btn key-clear" onclick="clearPin()">Clear</button>
|
||||
<button class="key-btn" onclick="enterPin('0')">0</button>
|
||||
<button class="key-btn key-enter" onclick="submitPin()">Enter</button>
|
||||
<div class="keypad" id="keypad">
|
||||
<!-- Keypad buttons will be generated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,19 +92,15 @@
|
||||
</div>
|
||||
<div class="menu-options">
|
||||
<button class="menu-btn" onclick="showView('withdrawView')">
|
||||
<!-- <span class="menu-icon">💵</span> -->
|
||||
<span class="menu-text">Withdraw</span>
|
||||
</button>
|
||||
<button class="menu-btn" onclick="showView('depositView')">
|
||||
<!-- <span class="menu-icon">💰</span> -->
|
||||
<span class="menu-text">Deposit</span>
|
||||
</button>
|
||||
<button class="menu-btn" onclick="showView('transferView')">
|
||||
<!-- <span class="menu-icon">↔️</span> -->
|
||||
<span class="menu-text">Transfer</span>
|
||||
</button>
|
||||
<!-- <button class="menu-btn" onclick="showView('depositView')"> -->
|
||||
<!-- <span class="menu-text">Deposit</span> -->
|
||||
<!-- </button> -->
|
||||
<!-- <button class="menu-btn" onclick="showView('transferView')"> -->
|
||||
<!-- <span class="menu-text">Transfer</span> -->
|
||||
<!-- </button> -->
|
||||
<button class="menu-btn" onclick="showView('balanceView')">
|
||||
<!-- <span class="menu-icon">📊</span> -->
|
||||
<span class="menu-text">Balance</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -143,7 +136,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Deposit Screen -->
|
||||
<div class="atm-view" id="depositView" style="display: none;">
|
||||
<!-- <div class="atm-view" id="depositView" style="display: none;">
|
||||
<h3>Deposit Cash</h3>
|
||||
<div class="deposit-display">
|
||||
<div class="deposit-info">
|
||||
@ -166,10 +159,10 @@
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- Transfer Screen -->
|
||||
<div class="atm-view" id="transferView" style="display: none;">
|
||||
<!-- <div class="atm-view" id="transferView" style="display: none;">
|
||||
<h3>Transfer Funds</h3>
|
||||
<div class="transfer-display">
|
||||
<div class="transfer-form">
|
||||
@ -193,7 +186,7 @@
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- Balance Screen -->
|
||||
<div class="atm-view" id="balanceView" style="display: none;">
|
||||
@ -236,7 +229,7 @@
|
||||
<h3>Transaction Failed</h3>
|
||||
<p id="errorMessage">An error occurred</p>
|
||||
</div>
|
||||
<button class="atm-btn atm-btn-secondary" onclick="showView('menuView')">
|
||||
<button class="atm-btn atm-btn-secondary" onclick="goBackFromError()">
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
@ -249,7 +242,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="atm.js"></script>
|
||||
<!-- <script src="atm.js"></script> -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -3,18 +3,17 @@
|
||||
* Handles banking transactions with PIN authentication
|
||||
*/
|
||||
|
||||
// Mock data
|
||||
const mockData = {
|
||||
cash: 2500,
|
||||
bank: 45750,
|
||||
pin: '1234' // For demo purposes
|
||||
};
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
let enteredPin = '';
|
||||
let currentView = 'welcomeView';
|
||||
let previousView = 'welcomeView';
|
||||
// ============================================================================
|
||||
// VIEW MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
// View Management
|
||||
function showView(viewId) {
|
||||
// Hide all views
|
||||
document.querySelectorAll('.atm-view').forEach(view => {
|
||||
@ -25,6 +24,7 @@ function showView(viewId) {
|
||||
const view = document.getElementById(viewId);
|
||||
if (view) {
|
||||
view.style.display = 'flex';
|
||||
previousView = currentView;
|
||||
currentView = viewId;
|
||||
|
||||
// Update balance displays when showing certain views
|
||||
@ -34,7 +34,52 @@ function showView(viewId) {
|
||||
}
|
||||
}
|
||||
|
||||
// PIN Entry
|
||||
// ============================================================================
|
||||
// PIN AUTHENTICATION
|
||||
// ============================================================================
|
||||
|
||||
function generateKeypad() {
|
||||
const keypad = document.getElementById('keypad');
|
||||
if (!keypad) return;
|
||||
|
||||
// Define keypad layout
|
||||
const keys = [
|
||||
{ value: '1', label: '1', type: 'number' },
|
||||
{ value: '2', label: '2', type: 'number' },
|
||||
{ value: '3', label: '3', type: 'number' },
|
||||
{ value: '4', label: '4', type: 'number' },
|
||||
{ value: '5', label: '5', type: 'number' },
|
||||
{ value: '6', label: '6', type: 'number' },
|
||||
{ value: '7', label: '7', type: 'number' },
|
||||
{ value: '8', label: '8', type: 'number' },
|
||||
{ value: '9', label: '9', type: 'number' },
|
||||
{ value: 'clear', label: 'Clear', type: 'action', class: 'key-clear' },
|
||||
{ value: '0', label: '0', type: 'number' },
|
||||
{ value: 'enter', label: 'Enter', type: 'action', class: 'key-enter' }
|
||||
];
|
||||
|
||||
// Clear existing keypad
|
||||
keypad.innerHTML = '';
|
||||
|
||||
// Generate buttons
|
||||
keys.forEach(key => {
|
||||
const button = document.createElement('button');
|
||||
button.className = `key-btn${key.class ? ' ' + key.class : ''}`;
|
||||
button.textContent = key.label;
|
||||
|
||||
// Add click handler
|
||||
if (key.type === 'number') {
|
||||
button.onclick = () => enterPin(key.value);
|
||||
} else if (key.value === 'clear') {
|
||||
button.onclick = () => clearPin();
|
||||
} else if (key.value === 'enter') {
|
||||
button.onclick = () => submitPin();
|
||||
}
|
||||
|
||||
keypad.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function enterPin(digit) {
|
||||
if (enteredPin.length < 4) {
|
||||
enteredPin += digit;
|
||||
@ -65,7 +110,8 @@ function submitPin() {
|
||||
}
|
||||
|
||||
// In a real implementation, this would validate with the server
|
||||
if (enteredPin === mockData.pin) {
|
||||
const currentState = store.getState();
|
||||
if (enteredPin === currentState.pin) {
|
||||
enteredPin = '';
|
||||
updatePinDisplay();
|
||||
showView('menuView');
|
||||
@ -75,40 +121,48 @@ function submitPin() {
|
||||
}
|
||||
}
|
||||
|
||||
// Balance Updates
|
||||
// ============================================================================
|
||||
// BALANCE MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
function updateBalances() {
|
||||
const currentState = store.getState();
|
||||
|
||||
// Update all balance displays
|
||||
const cashElements = ['cashBalance', 'cashBalanceDetail', 'availableCash'];
|
||||
const bankElements = ['bankBalance', 'bankBalanceDetail'];
|
||||
|
||||
cashElements.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = `$${mockData.cash.toLocaleString()}`;
|
||||
if (el) el.textContent = `$${currentState.accounts.cash.toLocaleString()}`;
|
||||
});
|
||||
|
||||
bankElements.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = `$${mockData.bank.toLocaleString()}`;
|
||||
if (el) el.textContent = `$${currentState.accounts.bank.toLocaleString()}`;
|
||||
});
|
||||
|
||||
const totalEl = document.getElementById('totalBalance');
|
||||
if (totalEl) {
|
||||
const total = mockData.cash + mockData.bank;
|
||||
const total = currentState.accounts.cash + currentState.accounts.bank;
|
||||
totalEl.textContent = `$${total.toLocaleString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Withdraw Functions
|
||||
// ============================================================================
|
||||
// WITHDRAW OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
function withdrawAmount(amount) {
|
||||
if (amount > mockData.bank) {
|
||||
const currentState = store.getState();
|
||||
|
||||
if (amount > currentState.accounts.bank) {
|
||||
showError('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
mockData.bank -= amount;
|
||||
mockData.cash += amount;
|
||||
|
||||
// sendEvent('atm::withdraw', { amount: amount });
|
||||
store.dispatch(withdraw(amount));
|
||||
sendEvent('atm::withdraw', { amount: amount });
|
||||
showSuccess(`Withdrew $${amount.toLocaleString()}`);
|
||||
}
|
||||
|
||||
@ -121,20 +175,26 @@ function withdrawCustom() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > mockData.bank) {
|
||||
const currentState = store.getState();
|
||||
if (amount > currentState.accounts.bank) {
|
||||
showError('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
mockData.bank -= amount;
|
||||
mockData.cash += amount;
|
||||
|
||||
// sendEvent('atm::withdraw', { amount: amount });
|
||||
store.dispatch(withdraw(amount));
|
||||
sendEvent('atm::withdraw', { amount: amount });
|
||||
input.value = '';
|
||||
showSuccess(`Withdrew $${amount.toLocaleString()}`);
|
||||
}
|
||||
|
||||
// Deposit Functions
|
||||
// ============================================================================
|
||||
// DEPOSIT OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Deposits specified amount into bank account
|
||||
* @deprecated Use store actions instead
|
||||
*/
|
||||
function depositAmount() {
|
||||
const input = document.getElementById('depositInput');
|
||||
const amount = parseFloat(input.value);
|
||||
@ -144,34 +204,42 @@ function depositAmount() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > mockData.cash) {
|
||||
const currentState = store.getState();
|
||||
if (amount > currentState.accounts.cash) {
|
||||
showError('Insufficient cash');
|
||||
return;
|
||||
}
|
||||
|
||||
mockData.cash -= amount;
|
||||
mockData.bank += amount;
|
||||
|
||||
// sendEvent('atm::deposit', { amount: amount });
|
||||
store.dispatch(deposit(amount));
|
||||
sendEvent('atm::deposit', { amount: amount });
|
||||
input.value = '';
|
||||
showSuccess(`Deposited $${amount.toLocaleString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deposits all available cash into bank account
|
||||
* @deprecated Use store actions instead
|
||||
*/
|
||||
function depositAll() {
|
||||
if (mockData.cash <= 0) {
|
||||
const currentState = store.getState();
|
||||
|
||||
if (currentState.accounts.cash <= 0) {
|
||||
showError('No cash to deposit');
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = mockData.cash;
|
||||
mockData.cash = 0;
|
||||
mockData.bank += amount;
|
||||
|
||||
// sendEvent('atm::deposit', { amount: amount });
|
||||
const amount = currentState.accounts.cash;
|
||||
store.dispatch(deposit(amount));
|
||||
sendEvent('atm::deposit', { amount: amount });
|
||||
showSuccess(`Deposited $${amount.toLocaleString()}`);
|
||||
}
|
||||
|
||||
// Transfer Function
|
||||
// ============================================================================
|
||||
// TRANSFER OPERATIONS
|
||||
// ============================================================================
|
||||
/**
|
||||
* Transfers specified amount from bank account to player account
|
||||
* @deprecated Use store actions instead
|
||||
*/
|
||||
function transferFunds() {
|
||||
const playerIdInput = document.getElementById('transferPlayerId');
|
||||
const amountInput = document.getElementById('transferAmount');
|
||||
@ -189,17 +257,17 @@ function transferFunds() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > mockData.bank) {
|
||||
const currentState = store.getState();
|
||||
if (amount > currentState.accounts.bank) {
|
||||
showError('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
mockData.bank -= amount;
|
||||
|
||||
// sendEvent('atm::transfer', {
|
||||
// playerId: playerId,
|
||||
// amount: amount
|
||||
// });
|
||||
store.dispatch(transfer('bank', amount, 'player'));
|
||||
sendEvent('atm::transfer', {
|
||||
playerId: playerId,
|
||||
amount: amount
|
||||
});
|
||||
|
||||
playerIdInput.value = '';
|
||||
amountInput.value = '';
|
||||
@ -207,7 +275,10 @@ function transferFunds() {
|
||||
showSuccess(`Transferred $${amount.toLocaleString()} to Player ${playerId}`);
|
||||
}
|
||||
|
||||
// Result Screens
|
||||
// ============================================================================
|
||||
// RESULT SCREENS
|
||||
// ============================================================================
|
||||
|
||||
function showSuccess(message) {
|
||||
document.getElementById('successMessage').textContent = message;
|
||||
showView('successView');
|
||||
@ -219,15 +290,36 @@ function showError(message) {
|
||||
showView('errorView');
|
||||
}
|
||||
|
||||
// Exit ATM
|
||||
function goBackFromError() {
|
||||
// If error happened during PIN entry, go back to PIN view
|
||||
// Otherwise go back to menu view
|
||||
if (previousView === 'pinView') {
|
||||
showView('pinView');
|
||||
} else {
|
||||
showView('menuView');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ATM CONTROL
|
||||
// ============================================================================
|
||||
|
||||
function exitATM() {
|
||||
enteredPin = '';
|
||||
updatePinDisplay();
|
||||
sendEvent('atm::exit', {});
|
||||
sendEvent('atm::close', {});
|
||||
showView('welcomeView');
|
||||
}
|
||||
|
||||
// Send event to Arma
|
||||
// ============================================================================
|
||||
// ARMA 3 INTEGRATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sends an event to Arma 3
|
||||
* @param {string} event - Event name
|
||||
* @param {Object} data - Event data
|
||||
*/
|
||||
function sendEvent(event, data) {
|
||||
if (typeof A3API !== 'undefined') {
|
||||
A3API.SendAlert(JSON.stringify({
|
||||
@ -239,20 +331,20 @@ function sendEvent(event, data) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update ATM data from external source
|
||||
function updateATMData(data) {
|
||||
if (data.cash !== undefined) {
|
||||
mockData.cash = data.cash;
|
||||
}
|
||||
if (data.bank !== undefined) {
|
||||
mockData.bank = data.bank;
|
||||
}
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
function initATM() {
|
||||
// Subscribe to store updates
|
||||
if (typeof store !== 'undefined') {
|
||||
store.subscribe(() => {
|
||||
updateBalances();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function initATM() {
|
||||
console.log('ATM interface initializing...');
|
||||
// Generate keypad
|
||||
generateKeypad();
|
||||
|
||||
// Show welcome screen
|
||||
showView('welcomeView');
|
||||
@ -260,7 +352,7 @@ function initATM() {
|
||||
// Update initial balances
|
||||
updateBalances();
|
||||
|
||||
console.log('ATM interface initialized');
|
||||
console.log('[ATM] Interface initialized');
|
||||
}
|
||||
|
||||
// Auto-initialize
|
||||
@ -270,8 +362,12 @@ if (document.readyState === 'loading') {
|
||||
initATM();
|
||||
}
|
||||
|
||||
// Expose functions globally
|
||||
// ============================================================================
|
||||
// GLOBAL EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
window.showView = showView;
|
||||
window.generateKeypad = generateKeypad;
|
||||
window.enterPin = enterPin;
|
||||
window.clearPin = clearPin;
|
||||
window.submitPin = submitPin;
|
||||
@ -280,5 +376,5 @@ window.withdrawCustom = withdrawCustom;
|
||||
window.depositAmount = depositAmount;
|
||||
window.depositAll = depositAll;
|
||||
window.transferFunds = transferFunds;
|
||||
window.goBackFromError = goBackFromError;
|
||||
window.exitATM = exitATM;
|
||||
window.updateATMData = updateATMData;
|
||||
|
||||
449
arma/client/addons/bank/ui/_site/bank.css
Normal file
449
arma/client/addons/bank/ui/_site/bank.css
Normal file
@ -0,0 +1,449 @@
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
.bank-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.bank-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);
|
||||
}
|
||||
|
||||
.bank-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;
|
||||
}
|
||||
|
||||
.bank-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bank-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;
|
||||
}
|
||||
|
||||
.bank-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&-primary {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.5);
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
border-color: rgba(200, 100, 100, 0.4);
|
||||
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
|
||||
.bank-content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr 350px;
|
||||
gap: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bank-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);
|
||||
|
||||
&-main {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
|
||||
&-track {
|
||||
background: rgba(15, 20, 30, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&-thumb {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(100, 150, 200, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-card {
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
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;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
.account-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
}
|
||||
|
||||
.account-type {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-balance {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid rgba(100, 150, 200, 0.2);
|
||||
|
||||
.balance-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-section {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-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;
|
||||
}
|
||||
|
||||
.transfer-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.form-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.9);
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-input {
|
||||
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;
|
||||
|
||||
&: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);
|
||||
}
|
||||
}
|
||||
|
||||
.form-select {
|
||||
padding-right: 2.5rem;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2396C8FF' d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 1rem center;
|
||||
background-size: 12px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
&::placeholder {
|
||||
color: rgba(100, 120, 140, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
margin: 0;
|
||||
|
||||
&::-webkit-inner-spin-button,
|
||||
&::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
.quick-action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(30, 45, 70, 0.8);
|
||||
border-color: rgba(150, 200, 255, 0.5);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.quick-action-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
color: rgba(180, 200, 220, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
.transaction-item {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.2);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.transaction-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.transaction-type {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
|
||||
&.deposit {
|
||||
background: rgba(100, 200, 150, 0.2);
|
||||
border: 1px solid rgba(100, 200, 150, 0.4);
|
||||
color: rgba(150, 255, 200, 0.9);
|
||||
}
|
||||
|
||||
&.withdrawal {
|
||||
background: rgba(200, 150, 100, 0.2);
|
||||
border: 1px solid rgba(200, 150, 100, 0.4);
|
||||
color: rgba(255, 200, 150, 0.9);
|
||||
}
|
||||
|
||||
&.transfer {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
color: rgba(150, 200, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-amount {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
|
||||
&.positive {
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: rgba(220, 100, 100, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.transaction-time {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(100, 150, 200, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.bank-content {
|
||||
grid-template-columns: 280px 1fr 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.bank-content {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
.panel-main {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Banking Services</title>
|
||||
<!-- <link rel="stylesheet" href="style.css" /> -->
|
||||
<!-- <script src="store.js"></script> -->
|
||||
<!-- <link rel="stylesheet" href="bank.css" /> -->
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
@ -14,19 +15,26 @@
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\style.css",
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\bank.css",
|
||||
),
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\script.js",
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\store.js",
|
||||
),
|
||||
]).then(([css, js]) => {
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\bank\\ui\\_site\\bank.js",
|
||||
),
|
||||
]).then(([css, storeJs, bankJs]) => {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
const store = document.createElement("script");
|
||||
store.text = storeJs;
|
||||
document.head.appendChild(store);
|
||||
|
||||
const bank = document.createElement("script");
|
||||
bank.text = bankJs;
|
||||
document.head.appendChild(bank);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
@ -36,7 +44,7 @@
|
||||
<!-- Header Section -->
|
||||
<div class="bank-header">
|
||||
<div class="bank-logo">
|
||||
<!-- <div class="logo-icon">💳</div> -->
|
||||
<!-- <img class="logo-icon" src="public/fdic.png" alt="Bank Logo" width="50"> -->
|
||||
</div>
|
||||
<div class="bank-info">
|
||||
<h1 class="bank-title">Banking Services</h1>
|
||||
@ -56,9 +64,8 @@
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<!-- Cash Account -->
|
||||
<div class="account-card active" data-account="cash">
|
||||
<div class="account-card">
|
||||
<div class="account-header">
|
||||
<!-- <span class="account-icon">💵</span> -->
|
||||
<div class="account-info">
|
||||
<span class="account-name">Cash</span>
|
||||
<span class="account-type">Physical Currency</span>
|
||||
@ -71,9 +78,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Bank Account -->
|
||||
<div class="account-card" data-account="bank">
|
||||
<div class="account-card">
|
||||
<div class="account-header">
|
||||
<!-- <span class="account-icon">🏦</span> -->
|
||||
<div class="account-info">
|
||||
<span class="account-name">Bank Account</span>
|
||||
<span class="account-type">Savings • Protected</span>
|
||||
@ -86,9 +92,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Organization Account -->
|
||||
<div class="account-card" data-account="org">
|
||||
<div class="account-card">
|
||||
<div class="account-header">
|
||||
<span class="account-icon">🏢</span>
|
||||
<div class="account-info">
|
||||
<span class="account-name">Organization</span>
|
||||
<span class="account-type">Shared • View Only</span>
|
||||
@ -115,26 +120,20 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">From</label>
|
||||
<select class="form-select" id="transferFrom">
|
||||
<option value="cash">Cash ($2,500)</option>
|
||||
<option value="bank" selected>Bank Account ($45,750)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">To</label>
|
||||
<select class="form-select" id="transferTo">
|
||||
<option value="bank" selected>Bank Account</option>
|
||||
<option value="cash">Cash</option>
|
||||
<option value="bank">Bank Account</option>
|
||||
<option value="player">Player</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Amount</label>
|
||||
<input type="number" class="form-input" id="transferAmount" placeholder="0.00" min="0"
|
||||
<input type="number" class="form-input" id="amount" placeholder="0.00" min="0"
|
||||
step="0.01">
|
||||
</div>
|
||||
<div class="form-group" id="playerIdGroup" style="display: none;">
|
||||
<label class="form-label">Player ID</label>
|
||||
<input type="text" class="form-input" id="playerId" placeholder="Enter player ID">
|
||||
<label class="form-label">Select Player</label>
|
||||
<select class="form-select" id="playerId">
|
||||
<option value="" disabled selected>Select a player...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -143,22 +142,18 @@
|
||||
<div class="action-section">
|
||||
<h3 class="section-title">Quick Access</h3>
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action-btn" data-action="deposit-amount">
|
||||
<span class="quick-action-label">Deposit</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" data-action="deposit">
|
||||
<!-- <span class="quick-action-icon">⬇️</span> -->
|
||||
<span class="quick-action-label">Deposit All Cash</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" data-action="withdraw">
|
||||
<!-- <span class="quick-action-icon">⬆️</span> -->
|
||||
<span class="quick-action-label">Withdraw</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" id="transferBtn">
|
||||
<!-- <span class="quick-action-icon">➡️</span> -->
|
||||
<span class="quick-action-label">Transfer Funds</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" data-action="statement">
|
||||
<!-- <span class="quick-action-icon">📄</span> -->
|
||||
<span class="quick-action-label">View Statement</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -171,67 +166,14 @@
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="transaction-list">
|
||||
<div class="transaction-item">
|
||||
<div class="transaction-header">
|
||||
<span class="transaction-type deposit">Deposit</span>
|
||||
<span class="transaction-amount positive">+$5,000</span>
|
||||
</div>
|
||||
<div class="transaction-details">
|
||||
<span class="transaction-desc">From Cash</span>
|
||||
<span class="transaction-time">2 hours ago</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transaction-item">
|
||||
<div class="transaction-header">
|
||||
<span class="transaction-type withdrawal">Withdrawal</span>
|
||||
<span class="transaction-amount negative">-$1,200</span>
|
||||
</div>
|
||||
<div class="transaction-details">
|
||||
<span class="transaction-desc">To Cash</span>
|
||||
<span class="transaction-time">5 hours ago</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transaction-item">
|
||||
<div class="transaction-header">
|
||||
<span class="transaction-type transfer">Transfer</span>
|
||||
<span class="transaction-amount negative">-$500</span>
|
||||
</div>
|
||||
<div class="transaction-details">
|
||||
<span class="transaction-desc">To Player #1234</span>
|
||||
<span class="transaction-time">1 day ago</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transaction-item">
|
||||
<div class="transaction-header">
|
||||
<span class="transaction-type deposit">Deposit</span>
|
||||
<span class="transaction-amount positive">+$10,000</span>
|
||||
</div>
|
||||
<div class="transaction-details">
|
||||
<span class="transaction-desc">Mission Reward</span>
|
||||
<span class="transaction-time">2 days ago</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="transaction-item">
|
||||
<div class="transaction-header">
|
||||
<span class="transaction-type transfer">Transfer</span>
|
||||
<span class="transaction-amount positive">+$2,000</span>
|
||||
</div>
|
||||
<div class="transaction-details">
|
||||
<span class="transaction-desc">From Player #5678</span>
|
||||
<span class="transaction-time">3 days ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <script src="script.js"></script> -->
|
||||
<!-- <script src="bank.js"></script> -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
281
arma/client/addons/bank/ui/_site/bank.js
Normal file
281
arma/client/addons/bank/ui/_site/bank.js
Normal file
@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Banking Interface
|
||||
* Handles transfers, deposits, withdrawals, and account management
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
function initBank() {
|
||||
setupEventHandlers();
|
||||
|
||||
// Subscribe to store updates
|
||||
if (typeof store !== 'undefined') {
|
||||
store.subscribe(() => {
|
||||
updateBalances();
|
||||
renderTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial render
|
||||
updateBalances();
|
||||
renderTransactions();
|
||||
|
||||
console.log('[Bank] Interface initialized');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
function setupEventHandlers() {
|
||||
// Close button
|
||||
const closeBtn = document.querySelector('.close-btn');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
sendEvent('bank::close', {});
|
||||
});
|
||||
}
|
||||
|
||||
// Transfer form
|
||||
const transferBtn = document.getElementById('transferBtn');
|
||||
const transferFrom = document.getElementById('transferFrom');
|
||||
const amount = document.getElementById('amount');
|
||||
const playerId = document.getElementById('playerId');
|
||||
const playerIdGroup = document.getElementById('playerIdGroup');
|
||||
|
||||
// Always show player ID field since transfer is only to players
|
||||
if (playerIdGroup) {
|
||||
playerIdGroup.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Transfer button
|
||||
if (transferBtn) {
|
||||
transferBtn.addEventListener('click', () => {
|
||||
const from = transferFrom.value;
|
||||
const transferAmount = parseFloat(amount.value);
|
||||
|
||||
if (!transferAmount || transferAmount <= 0) {
|
||||
console.log('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!playerId.value) {
|
||||
console.log('Please enter a player ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = store.getState();
|
||||
const fromAccountBalance = currentState.accounts[from];
|
||||
|
||||
if (transferAmount > fromAccountBalance) {
|
||||
console.log('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
const transferData = {
|
||||
from: from,
|
||||
amount: transferAmount,
|
||||
target: playerId.value
|
||||
};
|
||||
|
||||
sendEvent('bank::transfer', transferData);
|
||||
|
||||
// Dispatch to store to update UI
|
||||
store.dispatch(transfer(from, transferAmount, 'player'));
|
||||
|
||||
// Clear form
|
||||
amount.value = '';
|
||||
playerId.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Quick action buttons
|
||||
const quickActionBtns = document.querySelectorAll('.quick-action-btn');
|
||||
quickActionBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.dataset.action;
|
||||
const currentState = store.getState();
|
||||
|
||||
switch (action) {
|
||||
case 'deposit-amount':
|
||||
const depositAmountStr = document.getElementById('amount').value;
|
||||
if (depositAmountStr && parseFloat(depositAmountStr) > 0) {
|
||||
const depositAmount = parseFloat(depositAmountStr);
|
||||
if (depositAmount > currentState.accounts.cash) {
|
||||
console.log('Insufficient cash');
|
||||
return;
|
||||
}
|
||||
sendEvent('bank::deposit', { amount: depositAmount });
|
||||
store.dispatch(deposit(depositAmount));
|
||||
document.getElementById('amount').value = '';
|
||||
} else {
|
||||
console.log('Please enter a valid amount');
|
||||
}
|
||||
break;
|
||||
case 'deposit':
|
||||
const cashBalance = currentState.accounts.cash;
|
||||
if (cashBalance <= 0) {
|
||||
console.log('No cash to deposit');
|
||||
return;
|
||||
}
|
||||
sendEvent('bank::deposit', { amount: cashBalance });
|
||||
store.dispatch(deposit(cashBalance));
|
||||
break;
|
||||
case 'withdraw':
|
||||
const amountStr = document.getElementById('amount').value;
|
||||
if (amountStr && parseFloat(amountStr) > 0) {
|
||||
const withdrawAmount = parseFloat(amountStr);
|
||||
sendEvent('bank::withdraw', { amount: withdrawAmount });
|
||||
store.dispatch(withdraw(withdrawAmount));
|
||||
document.getElementById('amount').value = '';
|
||||
} else {
|
||||
console.log('Please enter a valid amount');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('Invalid action');
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UI UPDATES
|
||||
// ============================================================================
|
||||
|
||||
function updateBalances() {
|
||||
const currentState = store.getState();
|
||||
const balanceElements = document.querySelectorAll('.balance-amount');
|
||||
|
||||
// The HTML structure has 3 account cards.
|
||||
// 0: Cash, 1: Bank, 2: Org
|
||||
if (balanceElements.length >= 3) {
|
||||
balanceElements[0].textContent = `$${currentState.accounts.cash.toLocaleString()}`;
|
||||
balanceElements[1].textContent = `$${currentState.accounts.bank.toLocaleString()}`;
|
||||
balanceElements[2].textContent = `$${currentState.accounts.org.toLocaleString()}`;
|
||||
}
|
||||
|
||||
// Update form options
|
||||
const transferFrom = document.getElementById('transferFrom');
|
||||
|
||||
if (transferFrom) {
|
||||
const currentSelection = transferFrom.value;
|
||||
transferFrom.innerHTML = `
|
||||
<option value="cash">Cash</option>
|
||||
<option value="bank" selected>Bank Account</option>
|
||||
`;
|
||||
if (currentSelection && (currentSelection === 'cash' || currentSelection === 'bank')) {
|
||||
transferFrom.value = currentSelection;
|
||||
}
|
||||
}
|
||||
|
||||
// Update player list
|
||||
const playerSelect = document.getElementById('playerId');
|
||||
if (playerSelect && currentState.accounts.players) {
|
||||
const currentPlayerSelection = playerSelect.value;
|
||||
const players = currentState.accounts.players;
|
||||
const currentPlayerUid = currentState.uid;
|
||||
|
||||
// Clear existing options
|
||||
playerSelect.innerHTML = '<option value="">Select Player...</option>';
|
||||
|
||||
// Handle hashmap structure from Arma (UID -> {name, uid})
|
||||
if (players && typeof players === 'object') {
|
||||
// Convert hashmap to array and iterate
|
||||
Object.keys(players).forEach(uid => {
|
||||
// Skip current player to prevent self-transfers
|
||||
if (uid === currentPlayerUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playerData = players[uid];
|
||||
if (playerData && playerData.name) {
|
||||
const option = document.createElement('option');
|
||||
option.value = uid;
|
||||
option.textContent = playerData.name;
|
||||
playerSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (currentPlayerSelection) {
|
||||
// Verify if the selected player is still in the list
|
||||
const optionExists = Array.from(playerSelect.options).some(opt => opt.value === currentPlayerSelection);
|
||||
if (optionExists) {
|
||||
playerSelect.value = currentPlayerSelection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderTransactions() {
|
||||
const transactionList = document.querySelector('.transaction-list');
|
||||
if (!transactionList) return;
|
||||
|
||||
transactionList.innerHTML = '';
|
||||
|
||||
const currentState = store.getState();
|
||||
|
||||
currentState.transactions.forEach((transaction, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'transaction-item';
|
||||
|
||||
// Deposits are gains (green), Withdrawals and Transfers are losses (red)
|
||||
const isGain = transaction.type === 'Deposit';
|
||||
const amountClass = isGain ? 'positive' : 'negative';
|
||||
const displayAmount = isGain ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`;
|
||||
|
||||
// Map transaction types to CSS classes
|
||||
const typeClassMap = {
|
||||
'Deposit': 'deposit',
|
||||
'Withdraw': 'withdrawal',
|
||||
'Transfer': 'transfer'
|
||||
};
|
||||
const typeClass = typeClassMap[transaction.type] || transaction.type.toLowerCase();
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="transaction-header">
|
||||
<span class="transaction-type ${typeClass}">${transaction.type}</span>
|
||||
<span class="transaction-amount ${amountClass}">${displayAmount}</span>
|
||||
</div>
|
||||
<div class="transaction-details">
|
||||
<span class="transaction-time">${transaction.date}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
transactionList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARMA 3 INTEGRATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sends an event to Arma 3
|
||||
* @param {string} event - Event name
|
||||
* @param {Object} data - Event data
|
||||
*/
|
||||
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', initBank);
|
||||
} else {
|
||||
initBank();
|
||||
}
|
||||
BIN
arma/client/addons/bank/ui/_site/public/fdic.png
Normal file
BIN
arma/client/addons/bank/ui/_site/public/fdic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
arma/client/addons/bank/ui/_site/public/fdic_co.paa
Normal file
BIN
arma/client/addons/bank/ui/_site/public/fdic_co.paa
Normal file
Binary file not shown.
BIN
arma/client/addons/bank/ui/_site/public/fms.png
Normal file
BIN
arma/client/addons/bank/ui/_site/public/fms.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
arma/client/addons/bank/ui/_site/public/fms_co.paa
Normal file
BIN
arma/client/addons/bank/ui/_site/public/fms_co.paa
Normal file
Binary file not shown.
BIN
arma/client/addons/bank/ui/_site/public/gms.png
Normal file
BIN
arma/client/addons/bank/ui/_site/public/gms.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
arma/client/addons/bank/ui/_site/public/gms_co.paa
Normal file
BIN
arma/client/addons/bank/ui/_site/public/gms_co.paa
Normal file
Binary file not shown.
@ -1,248 +0,0 @@
|
||||
/**
|
||||
* Banking Interface
|
||||
* Handles transfers, deposits, withdrawals, and account management
|
||||
*/
|
||||
|
||||
// Mock data
|
||||
const mockData = {
|
||||
accounts: {
|
||||
cash: { name: "Cash", balance: 2500, type: "Physical Currency" },
|
||||
bank: { name: "Bank Account", balance: 45750, type: "Savings • Protected" },
|
||||
org: { name: "Organization", balance: 125000, type: "Shared • View Only", readOnly: true }
|
||||
},
|
||||
transactions: [
|
||||
{ type: "deposit", amount: 5000, desc: "From Cash", time: "2 hours ago" },
|
||||
{ type: "withdrawal", amount: -1200, desc: "To Cash", time: "5 hours ago" },
|
||||
{ type: "transfer", amount: -500, desc: "To Player #1234", time: "1 day ago" },
|
||||
{ type: "deposit", amount: 10000, desc: "Mission Reward", time: "2 days ago" },
|
||||
{ type: "transfer", amount: 2000, desc: "From Player #5678", time: "3 days ago" }
|
||||
]
|
||||
};
|
||||
|
||||
// State
|
||||
let selectedAccount = 'cash';
|
||||
|
||||
// Initialize
|
||||
function initBank() {
|
||||
console.log('Banking interface initializing...');
|
||||
|
||||
setupEventHandlers();
|
||||
updateBalances();
|
||||
|
||||
console.log('Banking interface initialized');
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
function setupEventHandlers() {
|
||||
// Close button
|
||||
const closeBtn = document.querySelector('.close-btn');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', () => {
|
||||
console.log('Closing bank...');
|
||||
sendEvent('bank::close', {});
|
||||
});
|
||||
}
|
||||
|
||||
// Account card selection
|
||||
const accountCards = document.querySelectorAll('.account-card');
|
||||
accountCards.forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
accountCards.forEach(c => c.classList.remove('active'));
|
||||
card.classList.add('active');
|
||||
selectedAccount = card.dataset.account;
|
||||
console.log('Selected account:', selectedAccount);
|
||||
});
|
||||
});
|
||||
|
||||
// Transfer form
|
||||
const transferBtn = document.getElementById('transferBtn');
|
||||
const transferFrom = document.getElementById('transferFrom');
|
||||
const transferTo = document.getElementById('transferTo');
|
||||
const transferAmount = document.getElementById('transferAmount');
|
||||
const playerId = document.getElementById('playerId');
|
||||
const playerIdGroup = document.getElementById('playerIdGroup');
|
||||
|
||||
// Show/hide player ID field
|
||||
if (transferTo) {
|
||||
transferTo.addEventListener('change', () => {
|
||||
if (transferTo.value === 'player') {
|
||||
playerIdGroup.style.display = 'flex';
|
||||
} else {
|
||||
playerIdGroup.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Transfer button
|
||||
if (transferBtn) {
|
||||
transferBtn.addEventListener('click', () => {
|
||||
const from = transferFrom.value;
|
||||
const to = transferTo.value;
|
||||
const amount = parseFloat(transferAmount.value);
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
alert('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
if (from === to) {
|
||||
alert('Source and destination must be different');
|
||||
return;
|
||||
}
|
||||
|
||||
const fromAccount = mockData.accounts[from];
|
||||
if (amount > fromAccount.balance) {
|
||||
alert('Insufficient funds');
|
||||
return;
|
||||
}
|
||||
|
||||
if (to === 'player' && !playerId.value) {
|
||||
alert('Please enter a player ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const transferData = {
|
||||
from: from,
|
||||
to: to,
|
||||
amount: amount,
|
||||
playerId: to === 'player' ? playerId.value : null
|
||||
};
|
||||
|
||||
console.log('Transfer request:', transferData);
|
||||
sendEvent('bank::transfer', transferData);
|
||||
|
||||
// Clear form
|
||||
transferAmount.value = '';
|
||||
if (to === 'player') playerId.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Quick action buttons
|
||||
const quickActionBtns = document.querySelectorAll('.quick-action-btn');
|
||||
quickActionBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.dataset.action;
|
||||
console.log('Quick action:', action);
|
||||
|
||||
switch (action) {
|
||||
case 'deposit':
|
||||
const cashBalance = mockData.accounts.cash.balance;
|
||||
if (cashBalance <= 0) {
|
||||
alert('No cash to deposit');
|
||||
return;
|
||||
}
|
||||
sendEvent('bank::deposit', { amount: cashBalance });
|
||||
break;
|
||||
case 'withdraw':
|
||||
const amount = prompt('Enter amount to withdraw:');
|
||||
if (amount && parseFloat(amount) > 0) {
|
||||
sendEvent('bank::withdraw', { amount: parseFloat(amount) });
|
||||
}
|
||||
break;
|
||||
case 'statement':
|
||||
sendEvent('bank::statement', {});
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Transaction items
|
||||
const transactionItems = document.querySelectorAll('.transaction-item');
|
||||
transactionItems.forEach((item, index) => {
|
||||
item.addEventListener('click', () => {
|
||||
console.log('Transaction clicked:', mockData.transactions[index]);
|
||||
sendEvent('bank::transaction::view', { transaction: mockData.transactions[index] });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update balances
|
||||
function updateBalances() {
|
||||
const balanceElements = document.querySelectorAll('.balance-amount');
|
||||
balanceElements[0].textContent = `$${mockData.accounts.cash.balance.toLocaleString()}`;
|
||||
balanceElements[1].textContent = `$${mockData.accounts.bank.balance.toLocaleString()}`;
|
||||
balanceElements[2].textContent = `$${mockData.accounts.org.balance.toLocaleString()}`;
|
||||
|
||||
// Update form options
|
||||
const transferFrom = document.getElementById('transferFrom');
|
||||
const transferTo = document.getElementById('transferTo');
|
||||
|
||||
if (transferFrom) {
|
||||
transferFrom.innerHTML = `
|
||||
<option value="cash">Cash ($${mockData.accounts.cash.balance.toLocaleString()})</option>
|
||||
<option value="bank" selected>Bank Account ($${mockData.accounts.bank.balance.toLocaleString()})</option>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Update bank data
|
||||
function updateBankData(data) {
|
||||
if (data.accounts) {
|
||||
Object.assign(mockData.accounts, data.accounts);
|
||||
updateBalances();
|
||||
}
|
||||
|
||||
if (data.transactions) {
|
||||
// Update transaction list
|
||||
mockData.transactions = data.transactions;
|
||||
// Re-render transaction list
|
||||
renderTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
// Render transactions
|
||||
function renderTransactions() {
|
||||
const transactionList = document.querySelector('.transaction-list');
|
||||
if (!transactionList) return;
|
||||
|
||||
transactionList.innerHTML = '';
|
||||
|
||||
mockData.transactions.forEach((transaction, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'transaction-item';
|
||||
|
||||
const isPositive = transaction.amount > 0;
|
||||
const amountClass = isPositive ? 'positive' : 'negative';
|
||||
const displayAmount = isPositive ? `+$${transaction.amount.toLocaleString()}` : `-$${Math.abs(transaction.amount).toLocaleString()}`;
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="transaction-header">
|
||||
<span class="transaction-type ${transaction.type}">${transaction.type}</span>
|
||||
<span class="transaction-amount ${amountClass}">${displayAmount}</span>
|
||||
</div>
|
||||
<div class="transaction-details">
|
||||
<span class="transaction-desc">${transaction.desc}</span>
|
||||
<span class="transaction-time">${transaction.time}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
console.log('Transaction clicked:', transaction);
|
||||
sendEvent('bank::transaction::view', { transaction: transaction });
|
||||
});
|
||||
|
||||
transactionList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// 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', initBank);
|
||||
} else {
|
||||
initBank();
|
||||
}
|
||||
|
||||
// Expose functions globally
|
||||
window.updateBankData = updateBankData;
|
||||
270
arma/client/addons/bank/ui/_site/store.js
Normal file
270
arma/client/addons/bank/ui/_site/store.js
Normal file
@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Banking Application Store
|
||||
* Redux-like state management for bank and ATM interfaces
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// REDUX CORE IMPLEMENTATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Creates a Redux-like store.
|
||||
* @param {Function} reducer - A function that returns the next state tree
|
||||
* @returns {Object} The store object with methods: getState, dispatch, subscribe
|
||||
*/
|
||||
function createStore(reducer) {
|
||||
let state;
|
||||
let listeners = [];
|
||||
|
||||
const getState = () => state;
|
||||
|
||||
const dispatch = (action) => {
|
||||
state = reducer(state, action);
|
||||
listeners.forEach(listener => listener());
|
||||
};
|
||||
|
||||
const subscribe = (listener) => {
|
||||
listeners.push(listener);
|
||||
return () => {
|
||||
listeners = listeners.filter(l => l !== listener);
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize state
|
||||
dispatch({});
|
||||
|
||||
return { getState, dispatch, subscribe };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
const initialState = {
|
||||
uid: '',
|
||||
accounts: {
|
||||
bank: 0,
|
||||
cash: 0,
|
||||
org: 0
|
||||
},
|
||||
pin: '1234',
|
||||
transactions: []
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// ACTION TYPES
|
||||
// ============================================================================
|
||||
|
||||
const DEPOSIT = 'DEPOSIT';
|
||||
const WITHDRAW = 'WITHDRAW';
|
||||
const TRANSFER = 'TRANSFER';
|
||||
const UPDATE_ACCOUNTS = 'UPDATE_ACCOUNTS';
|
||||
const UPDATE_PIN = 'UPDATE_PIN';
|
||||
|
||||
// ============================================================================
|
||||
// ACTION CREATORS
|
||||
// ============================================================================
|
||||
|
||||
const deposit = (amount) => ({
|
||||
type: DEPOSIT,
|
||||
payload: amount
|
||||
});
|
||||
|
||||
const withdraw = (amount) => ({
|
||||
type: WITHDRAW,
|
||||
payload: amount
|
||||
});
|
||||
|
||||
const transfer = (from, amount, target) => ({
|
||||
type: TRANSFER,
|
||||
from: from,
|
||||
payload: amount,
|
||||
target: target
|
||||
});
|
||||
|
||||
const updateAccounts = (accounts) => ({
|
||||
type: UPDATE_ACCOUNTS,
|
||||
payload: accounts
|
||||
});
|
||||
|
||||
const updatePin = (pin) => ({
|
||||
type: UPDATE_PIN,
|
||||
payload: pin
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// REDUCER
|
||||
// ============================================================================
|
||||
|
||||
function appReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case DEPOSIT:
|
||||
if (state.accounts.cash < action.payload) {
|
||||
console.warn('Insufficient cash!');
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
accounts: {
|
||||
...state.accounts,
|
||||
bank: state.accounts.bank + action.payload,
|
||||
cash: state.accounts.cash - action.payload
|
||||
},
|
||||
transactions: [
|
||||
...state.transactions,
|
||||
{
|
||||
type: 'Deposit',
|
||||
amount: action.payload,
|
||||
date: new Date().toLocaleString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
case WITHDRAW:
|
||||
if (state.accounts.bank < action.payload) {
|
||||
console.warn('Insufficient funds!');
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
accounts: {
|
||||
...state.accounts,
|
||||
bank: state.accounts.bank - action.payload,
|
||||
cash: state.accounts.cash + action.payload
|
||||
},
|
||||
transactions: [
|
||||
...state.transactions,
|
||||
{
|
||||
type: 'Withdraw',
|
||||
amount: action.payload,
|
||||
date: new Date().toLocaleString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
case TRANSFER:
|
||||
const fromAccount = action.from;
|
||||
if (state.accounts[fromAccount] < action.payload) {
|
||||
console.warn('Insufficient funds!');
|
||||
return state;
|
||||
}
|
||||
|
||||
const newAccounts = { ...state.accounts };
|
||||
newAccounts[fromAccount] -= action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
accounts: newAccounts,
|
||||
transactions: [
|
||||
...state.transactions,
|
||||
{
|
||||
type: 'Transfer',
|
||||
amount: action.payload,
|
||||
from: fromAccount,
|
||||
target: action.target,
|
||||
date: new Date().toLocaleString()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
case UPDATE_ACCOUNTS:
|
||||
return {
|
||||
...state,
|
||||
accounts: {
|
||||
...state.accounts,
|
||||
...action.payload
|
||||
}
|
||||
};
|
||||
|
||||
case UPDATE_PIN:
|
||||
return {
|
||||
...state,
|
||||
pin: String(action.payload)
|
||||
};
|
||||
|
||||
case 'SET_UID':
|
||||
return {
|
||||
...state,
|
||||
uid: action.payload
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STORE INSTANCE
|
||||
// ============================================================================
|
||||
|
||||
const store = createStore(appReducer);
|
||||
|
||||
// ============================================================================
|
||||
// ARMA 3 INTEGRATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sends an event to Arma 3
|
||||
* @param {string} event - Event name
|
||||
* @param {Object} data - Event data
|
||||
*/
|
||||
function sendEvent(event, data) {
|
||||
if (typeof A3API !== 'undefined') {
|
||||
A3API.SendAlert(JSON.stringify({
|
||||
event: event,
|
||||
data: data
|
||||
}));
|
||||
} else {
|
||||
console.log('Event:', event, 'Data:', data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs account data from Arma 3 into the store
|
||||
* @param {Object} data - Account data from Arma 3
|
||||
*/
|
||||
function syncDataFromArma(data) {
|
||||
if (data && typeof data === 'object') {
|
||||
const accounts = {};
|
||||
|
||||
if (data.cash !== undefined) accounts.cash = data.cash;
|
||||
if (data.bank !== undefined) accounts.bank = data.bank;
|
||||
if (data.org !== undefined) accounts.org = data.org;
|
||||
if (data.players !== undefined) accounts.players = data.players;
|
||||
|
||||
if (Object.keys(accounts).length > 0) {
|
||||
store.dispatch(updateAccounts(accounts));
|
||||
}
|
||||
|
||||
// Update UID if provided
|
||||
if (data.uid !== undefined && data.uid !== store.getState().uid) {
|
||||
store.dispatch({ type: 'SET_UID', payload: data.uid });
|
||||
}
|
||||
|
||||
// Update pin if provided
|
||||
if (data.pin !== undefined) {
|
||||
store.dispatch(updatePin(data.pin));
|
||||
}
|
||||
|
||||
console.log('[Store] Synced data from Arma:', store.getState().accounts);
|
||||
} else {
|
||||
console.warn('[Store] Invalid data received:', data);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
// Request initial data from Arma on load
|
||||
if (typeof A3API !== 'undefined') {
|
||||
// Delay request slightly to ensure everything is loaded
|
||||
setTimeout(() => {
|
||||
sendEvent('bank::sync', {});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Expose sync function globally for Arma to call
|
||||
if (typeof window !== 'undefined') {
|
||||
window.syncDataFromArma = syncDataFromArma;
|
||||
}
|
||||
@ -1,471 +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;
|
||||
}
|
||||
|
||||
.bank-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Header Section */
|
||||
.bank-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);
|
||||
}
|
||||
|
||||
.bank-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;
|
||||
}
|
||||
|
||||
.bank-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bank-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;
|
||||
}
|
||||
|
||||
.bank-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.action-btn-primary {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border-color: rgba(100, 150, 200, 0.5);
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn-primary:hover {
|
||||
background: rgba(100, 150, 200, 0.3);
|
||||
border-color: rgba(150, 200, 255, 0.7);
|
||||
}
|
||||
|
||||
.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 */
|
||||
.bank-content {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr 350px;
|
||||
gap: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.bank-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-main {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Account Cards */
|
||||
.account-card {
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
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;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.account-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.account-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);
|
||||
}
|
||||
|
||||
.account-card.active {
|
||||
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);
|
||||
}
|
||||
|
||||
.account-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.account-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.account-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
}
|
||||
|
||||
.account-type {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
}
|
||||
|
||||
.account-balance {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid rgba(100, 150, 200, 0.2);
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.8);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
/* Action Section */
|
||||
.action-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.action-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-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;
|
||||
}
|
||||
|
||||
/* Transfer Form */
|
||||
.transfer-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba(140, 160, 180, 0.9);
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-input {
|
||||
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;
|
||||
}
|
||||
|
||||
.form-select:focus,
|
||||
.form-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);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: rgba(100, 120, 140, 0.6);
|
||||
}
|
||||
|
||||
/* Quick Actions */
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.3);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
background: rgba(30, 45, 70, 0.8);
|
||||
border-color: rgba(150, 200, 255, 0.5);
|
||||
box-shadow:
|
||||
0 0 15px rgba(100, 150, 200, 0.15),
|
||||
inset 0 0 20px rgba(100, 150, 200, 0.05);
|
||||
}
|
||||
|
||||
.quick-action-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.quick-action-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
color: rgba(180, 200, 220, 0.9);
|
||||
}
|
||||
|
||||
/* Transaction List */
|
||||
.transaction-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
padding: 1rem;
|
||||
background: rgba(20, 30, 45, 0.6);
|
||||
border: 1px solid rgba(100, 150, 200, 0.2);
|
||||
border-left: 3px solid rgba(100, 150, 200, 0.4);
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.transaction-item:hover {
|
||||
background: rgba(30, 45, 70, 0.7);
|
||||
border-left-color: rgba(150, 200, 255, 0.6);
|
||||
}
|
||||
|
||||
.transaction-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.transaction-type {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.transaction-type.deposit {
|
||||
background: rgba(100, 200, 150, 0.2);
|
||||
border: 1px solid rgba(100, 200, 150, 0.4);
|
||||
color: rgba(150, 255, 200, 0.9);
|
||||
}
|
||||
|
||||
.transaction-type.withdrawal {
|
||||
background: rgba(200, 150, 100, 0.2);
|
||||
border: 1px solid rgba(200, 150, 100, 0.4);
|
||||
color: rgba(255, 200, 150, 0.9);
|
||||
}
|
||||
|
||||
.transaction-type.transfer {
|
||||
background: rgba(100, 150, 200, 0.2);
|
||||
border: 1px solid rgba(100, 150, 200, 0.4);
|
||||
color: rgba(150, 200, 255, 0.9);
|
||||
}
|
||||
|
||||
.transaction-amount {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.transaction-amount.positive {
|
||||
color: rgba(100, 200, 150, 1);
|
||||
}
|
||||
|
||||
.transaction-amount.negative {
|
||||
color: rgba(200, 150, 100, 1);
|
||||
}
|
||||
|
||||
.transaction-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.transaction-desc {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(180, 200, 220, 0.9);
|
||||
}
|
||||
|
||||
.transaction-time {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(100, 150, 200, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 1400px) {
|
||||
.bank-content {
|
||||
grid-template-columns: 280px 1fr 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.bank-content {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
.panel-main {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
12
arma/client/addons/common/CfgVehicles.hpp
Normal file
12
arma/client/addons/common/CfgVehicles.hpp
Normal file
@ -0,0 +1,12 @@
|
||||
class CfgVehicles {
|
||||
class Land_Bodybag_01_black_F;
|
||||
class forge_bodyBag: Land_Bodybag_01_black_F {
|
||||
maximumLoad = 2000;
|
||||
transportMaxWeapons = 500;
|
||||
transportMaxMagazines = 2000;
|
||||
transportMaxItems = 1000;
|
||||
ace_dragging_canCarry = 1;
|
||||
ace_dragging_carryPosition[] = {0, 0.5, 1.2};
|
||||
ace_dragging_carryDirection = 90;
|
||||
};
|
||||
};
|
||||
@ -17,3 +17,4 @@ class CfgPatches {
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "CfgVehicles.hpp"
|
||||
|
||||
1
arma/client/addons/garage/$PBOPREFIX$
Normal file
1
arma/client/addons/garage/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\garage
|
||||
19
arma/client/addons/garage/CfgEventHandlers.hpp
Normal file
19
arma/client/addons/garage/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,19 @@
|
||||
class Extended_PreStart_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
||||
};
|
||||
};
|
||||
4
arma/client/addons/garage/README.md
Normal file
4
arma/client/addons/garage/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
forge_client_garage
|
||||
===================
|
||||
|
||||
Description for this addon
|
||||
3
arma/client/addons/garage/XEH_PREP.hpp
Normal file
3
arma/client/addons/garage/XEH_PREP.hpp
Normal file
@ -0,0 +1,3 @@
|
||||
PREP(initGarageClass);
|
||||
PREP(initVGClass);
|
||||
PREP(openVG);
|
||||
1
arma/client/addons/garage/XEH_postInit.sqf
Normal file
1
arma/client/addons/garage/XEH_postInit.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
48
arma/client/addons/garage/XEH_postInitClient.sqf
Normal file
48
arma/client/addons/garage/XEH_postInitClient.sqf
Normal file
@ -0,0 +1,48 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(GarageClass)) then { [] call FUNC(initGarageClass); };
|
||||
if (isNil QGVAR(VGarageClass)) then { [] call FUNC(initVGClass); };
|
||||
|
||||
[QGVAR(initGarage), {
|
||||
GVAR(GarageClass) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitGarage), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(GarageClass) call ["sync", [_data, true]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncGarage), {
|
||||
params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(GarageClass) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(initVG), {
|
||||
GVAR(VGarageClass) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitVG), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(VGarageClass) call ["sync", [_data, true]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncVG), {
|
||||
params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(VGarageClass) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(bank,BankClass) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initGarage), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
[{
|
||||
GVAR(GarageClass) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initVG), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
17
arma/client/addons/garage/XEH_preInit.sqf
Normal file
17
arma/client/addons/garage/XEH_preInit.sqf
Normal file
@ -0,0 +1,17 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
// #include "initSettings.inc.sqf"
|
||||
// #include "initKeybinds.inc.sqf"
|
||||
|
||||
GVAR(Cars) = [];
|
||||
GVAR(Armor) = [];
|
||||
GVAR(Helis) = [];
|
||||
GVAR(Planes) = [];
|
||||
GVAR(Naval) = [];
|
||||
GVAR(Other) = [];
|
||||
1
arma/client/addons/garage/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/garage/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
3
arma/client/addons/garage/XEH_preStart.sqf
Normal file
3
arma/client/addons/garage/XEH_preStart.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
19
arma/client/addons/garage/config.cpp
Normal file
19
arma/client/addons/garage/config.cpp
Normal file
@ -0,0 +1,19 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"IDSolutions"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
17
arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
Normal file
17
arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
Normal file
@ -0,0 +1,17 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Handles the UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_garage_fnc_handleUIEvents;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
73
arma/client/addons/garage/functions/fnc_initGarageClass.sqf
Normal file
73
arma/client/addons/garage/functions/fnc_initGarageClass.sqf
Normal file
@ -0,0 +1,73 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Initializes the garage class.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_garage_fnc_initGarageClass;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(GarageClass) = createHashMapObject [[
|
||||
["#type", "IGarageClass"],
|
||||
["#create", {
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["garage", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", {
|
||||
private _uid = _self get "uid";
|
||||
private _garage = _self get "garage";
|
||||
|
||||
[SRPC(garage,requestInitGarage), [_uid, _garage]] call CFUNC(serverEvent);
|
||||
|
||||
systemChat format ["Garage loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Garage] Garage Class Initialized!";
|
||||
}],
|
||||
["save", {
|
||||
params [["_sync", false, [false]]];
|
||||
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(garage,requestSaveGarage), [_uid, _sync]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_sync", false, [false]]];
|
||||
|
||||
private _garage = _self get "garage";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
|
||||
if (_data isEqualTo createHashMap) exitWith {
|
||||
diag_log "[FORGE:Client:Garage] Empty data received for sync, skipping.";
|
||||
};
|
||||
|
||||
{
|
||||
_garage set [_x, _y];
|
||||
} forEach _data;
|
||||
|
||||
_self set ["garage", _garage];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Garage] Sync completed";
|
||||
}],
|
||||
["get", {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
private _garage = _self get "garage";
|
||||
_garage getOrDefault [_key, _default];
|
||||
}]
|
||||
]];
|
||||
|
||||
SETVAR(player,FORGE_GarageClass,GVAR(GarageClass));
|
||||
GVAR(GarageClass)
|
||||
116
arma/client/addons/garage/functions/fnc_initVGClass.sqf
Normal file
116
arma/client/addons/garage/functions/fnc_initVGClass.sqf
Normal file
@ -0,0 +1,116 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initVGarageClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-16
|
||||
* Last Update: 2025-12-19
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the Virtual Garage class for managing player garage unlocks.
|
||||
* Provides methods for syncing, saving, and applying virtual items to BIS Garage.
|
||||
*
|
||||
* Parameter(s):
|
||||
* None
|
||||
*
|
||||
* Returns:
|
||||
* vGarage class object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example(s):
|
||||
* [] call forge_client_locker_fnc_initVGClass;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(VGarageClass) = createHashMapObject [[
|
||||
["#type", "IVGarageClass"],
|
||||
["#create", {
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["vGarage", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
|
||||
private _vGarage = createHashMap;
|
||||
_vGarage set ["cars", ["B_Quadbike_01_F"]];
|
||||
_vGarage set ["armor", []];
|
||||
_vGarage set ["helis", []];
|
||||
_vGarage set ["planes", []];
|
||||
_vGarage set ["naval", []];
|
||||
_vGarage set ["other", []];
|
||||
|
||||
_self set ["vGarage", _vGarage];
|
||||
}],
|
||||
["init", {
|
||||
private _uid = _self get "uid";
|
||||
private _vGarage = _self get "vGarage";
|
||||
|
||||
[SRPC(garage,requestInitVG), [_uid, _vGarage]] call CFUNC(serverEvent);
|
||||
|
||||
systemChat format ["VGarage loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:VGarage] VGarage Class Initialized!";
|
||||
}],
|
||||
["save", {
|
||||
params [["_sync", false, [false]]];
|
||||
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(garage,requestSaveVG), [_uid, _sync]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
private _vGarage = _self get "vGarage";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
|
||||
if (_data isEqualTo createHashMap) exitWith { diag_log "[FORGE:Client:VGarage] Empty data received for sync, skipping."; };
|
||||
|
||||
{
|
||||
_vGarage set [_x, _y];
|
||||
|
||||
if (_jip) then {
|
||||
switch (_x) do {
|
||||
case "cars": { _self call ["apply", ["cars"]]; };
|
||||
case "armor": { _self call ["apply", ["armor"]]; };
|
||||
case "helis": { _self call ["apply", ["helis"]]; };
|
||||
case "planes": { _self call ["apply", ["planes"]]; };
|
||||
case "naval": { _self call ["apply", ["naval"]]; };
|
||||
case "other": { _self call ["apply", ["other"]]; };
|
||||
default {};
|
||||
};
|
||||
};
|
||||
} forEach _data;
|
||||
|
||||
_self set ["vGarage", _vGarage];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:VGarage] Sync completed";
|
||||
}],
|
||||
["get", {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
private _vGarage = _self get "vGarage";
|
||||
_vGarage getOrDefault [_key, _default];
|
||||
}],
|
||||
["apply", {
|
||||
params [["_key", "", [""]]];
|
||||
|
||||
private _vehicles = _self call ["get", [_key, []]];
|
||||
private _array = switch (_key) do {
|
||||
case "cars": { GVAR(Cars) };
|
||||
case "armor": { GVAR(Armor) };
|
||||
case "helis": { GVAR(Helis) };
|
||||
case "planes": { GVAR(Planes) };
|
||||
case "naval": { GVAR(Naval) };
|
||||
case "other": { GVAR(Other) };
|
||||
default { [] };
|
||||
};
|
||||
|
||||
{
|
||||
_array append [getText (configFile >> "CfgVehicles" >> _x >> "model"), [configFile >> "CfgVehicles" >> _x]];
|
||||
} forEach _vehicles;
|
||||
}]
|
||||
]];
|
||||
|
||||
SETVAR(player,FORGE_VGarageClass,GVAR(VGarageClass));
|
||||
GVAR(VGarageClass)
|
||||
17
arma/client/addons/garage/functions/fnc_openUI.sqf
Normal file
17
arma/client/addons/garage/functions/fnc_openUI.sqf
Normal file
@ -0,0 +1,17 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Opens the garage UI.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_garage_fnc_openUI;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
83
arma/client/addons/garage/functions/fnc_openVG.sqf
Normal file
83
arma/client/addons/garage/functions/fnc_openVG.sqf
Normal file
@ -0,0 +1,83 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initVG.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-16
|
||||
* Last Update: 2025-12-17
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* No description added yet.
|
||||
*
|
||||
* Parameter(s):
|
||||
* N/A
|
||||
*
|
||||
* Returns:
|
||||
* Something [BOOL]
|
||||
*
|
||||
* Example(s):
|
||||
* [parameter] call forge_x_component_fnc_myFunction
|
||||
*/
|
||||
|
||||
private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
|
||||
{
|
||||
FORGE_VehSpawnPos = (_x select 1) getPos [5, (_x select 2)];
|
||||
true;
|
||||
} count _locations;
|
||||
|
||||
BIS_fnc_garage_center = createVehicle ["Land_HelipadEmpty_F", FORGE_VehSpawnPos, [], 0, "NONE"];
|
||||
|
||||
[missionNamespace, "garageOpened", {
|
||||
params ["_display", "_toggleSpace"];
|
||||
|
||||
missionNamespace setVariable ["BIS_fnc_garage_data", [
|
||||
GVAR(Cars),
|
||||
GVAR(Armor),
|
||||
GVAR(Helis),
|
||||
GVAR(Planes),
|
||||
GVAR(Naval),
|
||||
GVAR(Other)
|
||||
]];
|
||||
|
||||
{
|
||||
lbClear (_display displayCtrl (960 + _forEachIndex));
|
||||
} forEach BIS_fnc_garage_data;
|
||||
|
||||
["ListAdd", [_display]] call BFUNC(garage);
|
||||
}] call BFUNC(addScriptedEventHandler);
|
||||
|
||||
BIS_fnc_garage_centerType = getText (configFile >> "CfgVehicles" >> "B_Quadbike_01_F" >> "model");
|
||||
|
||||
["Open", true] call BFUNC(garage);
|
||||
|
||||
[missionNamespace, "garageClosed", {
|
||||
private _nearestObjects = BIS_fnc_garage_center nearEntities [["Car","Tank","Air","Ship"], 15];
|
||||
|
||||
if (!isNil "_nearestObjects") then {
|
||||
private _obj = _nearestObjects select 0;
|
||||
private _veh = typeOf _obj;
|
||||
private _textures = getObjectTextures _obj;
|
||||
private _animationNames = animationNames _obj;
|
||||
|
||||
{ deleteVehicle _x } forEach _nearestObjects;
|
||||
private _createVehicle = createVehicle [_veh, FORGE_VehSpawnPos, [], 0, "CAN_COLLIDE"];
|
||||
|
||||
if (_textures isNotEqualTo []) then {
|
||||
private _count = 0;
|
||||
{
|
||||
_createVehicle setObjectTextureGlobal [_count, _x];
|
||||
_count = _count + 1;
|
||||
} forEach _textures;
|
||||
};
|
||||
|
||||
if (_animationNames isNotEqualTo []) then {
|
||||
private _animationPhase = [];
|
||||
|
||||
for "_i" from 0 to count _animationNames -1 do {
|
||||
_animationPhase pushBack [_animationNames select _i, _obj animationPhase (_animationNames select _i)];
|
||||
{ _createVehicle animate _x; } forEach _animationPhase;
|
||||
};
|
||||
};
|
||||
};
|
||||
}] call BFUNC(addScriptedEventHandler);
|
||||
9
arma/client/addons/garage/script_component.hpp
Normal file
9
arma/client/addons/garage/script_component.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#define COMPONENT garage
|
||||
#define COMPONENT_BEAUTIFIED Garage
|
||||
#include "\forge\forge_client\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#include "\forge\forge_client\addons\main\script_macros.hpp"
|
||||
8
arma/client/addons/garage/stringtable.xml
Normal file
8
arma/client/addons/garage/stringtable.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project name="FFE">
|
||||
<Package name="Garage">
|
||||
<Key ID="STR_forge_client_garage_displayName">
|
||||
<English>Garage</English>
|
||||
</Key>
|
||||
</Package>
|
||||
</Project>
|
||||
98
arma/client/addons/garage/ui/RscCommon.hpp
Normal file
98
arma/client/addons/garage/ui/RscCommon.hpp
Normal file
@ -0,0 +1,98 @@
|
||||
// Control types
|
||||
#define CT_STATIC 0
|
||||
#define CT_BUTTON 1
|
||||
#define CT_EDIT 2
|
||||
#define CT_SLIDER 3
|
||||
#define CT_COMBO 4
|
||||
#define CT_LISTBOX 5
|
||||
#define CT_TOOLBOX 6
|
||||
#define CT_CHECKBOXES 7
|
||||
#define CT_PROGRESS 8
|
||||
#define CT_HTML 9
|
||||
#define CT_STATIC_SKEW 10
|
||||
#define CT_ACTIVETEXT 11
|
||||
#define CT_TREE 12
|
||||
#define CT_STRUCTURED_TEXT 13
|
||||
#define CT_CONTEXT_MENU 14
|
||||
#define CT_CONTROLS_GROUP 15
|
||||
#define CT_SHORTCUTBUTTON 16
|
||||
#define CT_HITZONES 17
|
||||
#define CT_XKEYDESC 40
|
||||
#define CT_XBUTTON 41
|
||||
#define CT_XLISTBOX 42
|
||||
#define CT_XSLIDER 43
|
||||
#define CT_XCOMBO 44
|
||||
#define CT_ANIMATED_TEXTURE 45
|
||||
#define CT_OBJECT 80
|
||||
#define CT_OBJECT_ZOOM 81
|
||||
#define CT_OBJECT_CONTAINER 82
|
||||
#define CT_OBJECT_CONT_ANIM 83
|
||||
#define CT_LINEBREAK 98
|
||||
#define CT_USER 99
|
||||
#define CT_MAP 100
|
||||
#define CT_MAP_MAIN 101
|
||||
#define CT_LISTNBOX 102
|
||||
#define CT_ITEMSLOT 103
|
||||
#define CT_CHECKBOX 77
|
||||
|
||||
// Static styles
|
||||
#define ST_POS 0x0F
|
||||
#define ST_HPOS 0x03
|
||||
#define ST_VPOS 0x0C
|
||||
#define ST_LEFT 0x00
|
||||
#define ST_RIGHT 0x01
|
||||
#define ST_CENTER 0x02
|
||||
#define ST_DOWN 0x04
|
||||
#define ST_UP 0x08
|
||||
#define ST_VCENTER 0x0C
|
||||
|
||||
#define ST_TYPE 0xF0
|
||||
#define ST_SINGLE 0x00
|
||||
#define ST_MULTI 0x10
|
||||
#define ST_TITLE_BAR 0x20
|
||||
#define ST_PICTURE 0x30
|
||||
#define ST_FRAME 0x40
|
||||
#define ST_BACKGROUND 0x50
|
||||
#define ST_GROUP_BOX 0x60
|
||||
#define ST_GROUP_BOX2 0x70
|
||||
#define ST_HUD_BACKGROUND 0x80
|
||||
#define ST_TILE_PICTURE 0x90
|
||||
#define ST_WITH_RECT 0xA0
|
||||
#define ST_LINE 0xB0
|
||||
#define ST_UPPERCASE 0xC0
|
||||
#define ST_LOWERCASE 0xD0
|
||||
|
||||
#define ST_SHADOW 0x100
|
||||
#define ST_NO_RECT 0x200
|
||||
#define ST_KEEP_ASPECT_RATIO 0x800
|
||||
|
||||
// Slider styles
|
||||
#define SL_DIR 0x400
|
||||
#define SL_VERT 0
|
||||
#define SL_HORZ 0x400
|
||||
|
||||
#define SL_TEXTURES 0x10
|
||||
|
||||
// progress bar
|
||||
#define ST_VERTICAL 0x01
|
||||
#define ST_HORIZONTAL 0
|
||||
|
||||
// Listbox styles
|
||||
#define LB_TEXTURES 0x10
|
||||
#define LB_MULTI 0x20
|
||||
|
||||
// Tree styles
|
||||
#define TR_SHOWROOT 1
|
||||
#define TR_AUTOCOLLAPSE 2
|
||||
|
||||
// Default text sizes
|
||||
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
|
||||
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
|
||||
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
|
||||
|
||||
// Pixel grid
|
||||
#define pixelScale 0.50
|
||||
#define GRID_W (pixelW * pixelGrid * pixelScale)
|
||||
#define GRID_H (pixelH * pixelGrid * pixelScale)
|
||||
|
||||
class RscText;
|
||||
21
arma/client/addons/garage/ui/RscGarage.hpp
Normal file
21
arma/client/addons/garage/ui/RscGarage.hpp
Normal file
@ -0,0 +1,21 @@
|
||||
class RscGarage {
|
||||
idd = 1000;
|
||||
fadeIn = 0;
|
||||
fadeOut = 0;
|
||||
duration = 1e011;
|
||||
onLoad = "uiNamespace setVariable ['RscGarage', _this select 0]";
|
||||
onUnLoad = "uinamespace setVariable ['RscGarage', nil]";
|
||||
|
||||
class controlsBackground {};
|
||||
class controls {
|
||||
class IFrame: RscText {
|
||||
type = 106;
|
||||
idc = 1001;
|
||||
x = "safeZoneXAbs";
|
||||
y = "safeZoneY";
|
||||
w = "safeZoneWAbs";
|
||||
h = "safeZoneH";
|
||||
colorBackground[] = {0, 0, 0, 0};
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vehicle Garage</title>
|
||||
<link rel="stylesheet" href="garage.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<!--
|
||||
Dynamic Resource Loading
|
||||
The following script loads CSS and JavaScript files dynamically using the A3API
|
||||
@ -14,10 +14,10 @@
|
||||
<script>
|
||||
Promise.all([
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\actor\\ui\\_site\\garage.css",
|
||||
"forge\\forge_client\\addons\\garage\\ui\\_site\\style.css",
|
||||
),
|
||||
A3API.RequestFile(
|
||||
"forge\\forge_client\\addons\\actor\\ui\\_site\\garage.js",
|
||||
"forge\\forge_client\\addons\\garage\\ui\\_site\\script.js",
|
||||
),
|
||||
]).then(([css, js]) => {
|
||||
const style = document.createElement("style");
|
||||
@ -199,7 +199,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="garage.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1
arma/client/addons/locker/$PBOPREFIX$
Normal file
1
arma/client/addons/locker/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\locker
|
||||
19
arma/client/addons/locker/CfgEventHandlers.hpp
Normal file
19
arma/client/addons/locker/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,19 @@
|
||||
class Extended_PreStart_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
||||
};
|
||||
};
|
||||
4
arma/client/addons/locker/README.md
Normal file
4
arma/client/addons/locker/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
forge_client_locker
|
||||
===================
|
||||
|
||||
Description for this addon
|
||||
2
arma/client/addons/locker/XEH_PREP.hpp
Normal file
2
arma/client/addons/locker/XEH_PREP.hpp
Normal file
@ -0,0 +1,2 @@
|
||||
PREP(initLockerClass);
|
||||
PREP(initVAClass);
|
||||
1
arma/client/addons/locker/XEH_postInit.sqf
Normal file
1
arma/client/addons/locker/XEH_postInit.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
48
arma/client/addons/locker/XEH_postInitClient.sqf
Normal file
48
arma/client/addons/locker/XEH_postInitClient.sqf
Normal file
@ -0,0 +1,48 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(LockerClass)) then { [] call FUNC(initLockerClass); };
|
||||
if (isNil QGVAR(VArsenalClass)) then { [] call FUNC(initVAClass); };
|
||||
|
||||
[QGVAR(initLocker), {
|
||||
GVAR(LockerClass) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitLocker), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(LockerClass) call ["sync", [_data, true]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncLocker), {
|
||||
params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(LockerClass) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(initVA), {
|
||||
GVAR(VArsenalClass) call ["init", []];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseInitVA), {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(VArsenalClass) call ["sync", [_data, true]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncVA), {
|
||||
params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]];
|
||||
|
||||
GVAR(VArsenalClass) call ["sync", [_data, _jip]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[{
|
||||
EGVAR(garage,GarageClass) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initLocker), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
[{
|
||||
GVAR(LockerClass) get "isLoaded";
|
||||
}, {
|
||||
[QGVAR(initVA), []] call CFUNC(localEvent);
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
10
arma/client/addons/locker/XEH_preInit.sqf
Normal file
10
arma/client/addons/locker/XEH_preInit.sqf
Normal file
@ -0,0 +1,10 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
// #include "initSettings.inc.sqf"
|
||||
// #include "initKeybinds.inc.sqf"
|
||||
1
arma/client/addons/locker/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/locker/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
3
arma/client/addons/locker/XEH_preStart.sqf
Normal file
3
arma/client/addons/locker/XEH_preStart.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
19
arma/client/addons/locker/config.cpp
Normal file
19
arma/client/addons/locker/config.cpp
Normal file
@ -0,0 +1,19 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"IDSolutions"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
17
arma/client/addons/locker/functions/fnc_handleUIEvents.sqf
Normal file
17
arma/client/addons/locker/functions/fnc_handleUIEvents.sqf
Normal file
@ -0,0 +1,17 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Handles the UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_locker_fnc_handleUIEvents;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
73
arma/client/addons/locker/functions/fnc_initLockerClass.sqf
Normal file
73
arma/client/addons/locker/functions/fnc_initLockerClass.sqf
Normal file
@ -0,0 +1,73 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Initializes the locker class.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_locker_fnc_initLockerClass;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(LockerClass) = createHashMapObject [[
|
||||
["#type", "ILockerClass"],
|
||||
["#create", {
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["locker", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["init", {
|
||||
private _uid = _self get "uid";
|
||||
private _locker = _self get "locker";
|
||||
|
||||
[SRPC(locker,requestInitLocker), [_uid, _locker]] call CFUNC(serverEvent);
|
||||
|
||||
systemChat format ["Locker loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Locker] Locker Class Initialized!";
|
||||
}],
|
||||
["save", {
|
||||
params [["_sync", false, [false]]];
|
||||
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(locker,requestSaveLocker), [_uid, _sync]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
private _locker = _self get "locker";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
|
||||
if (_data isEqualTo createHashMap) exitWith {
|
||||
diag_log "[FORGE:Client:Locker] Empty data received for sync, skipping.";
|
||||
};
|
||||
|
||||
{
|
||||
_locker set [_x, _y];
|
||||
} forEach _data;
|
||||
|
||||
_self set ["locker", _locker];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Locker] Sync completed";
|
||||
}],
|
||||
["get", {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
private _locker = _self get "locker";
|
||||
_locker getOrDefault [_key, _default];
|
||||
}]
|
||||
]];
|
||||
|
||||
SETVAR(player,FORGE_LockerClass,GVAR(LockerClass));
|
||||
GVAR(LockerClass)
|
||||
111
arma/client/addons/locker/functions/fnc_initVAClass.sqf
Normal file
111
arma/client/addons/locker/functions/fnc_initVAClass.sqf
Normal file
@ -0,0 +1,111 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initVAClass.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-16
|
||||
* Last Update: 2025-12-17
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the Virtual Arsenal class for managing player arsenal unlocks.
|
||||
* Provides methods for syncing, saving, and applying virtual items to BIS Arsenal.
|
||||
*
|
||||
* Parameter(s):
|
||||
* None
|
||||
*
|
||||
* Returns:
|
||||
* vArsenal class object [HASHMAP OBJECT]
|
||||
*
|
||||
* Example(s):
|
||||
* [] call forge_client_locker_fnc_initVAClass;
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(VArsenalClass) = createHashMapObject [[
|
||||
["#type", "IVArsenalClass"],
|
||||
["#create", {
|
||||
_self set ["uid", (getPlayerUID player)];
|
||||
_self set ["vArsenal", createHashMap];
|
||||
_self set ["isLoaded", false];
|
||||
_self set ["lastSave", time];
|
||||
|
||||
private _vArsenal = createHashMap;
|
||||
_vArsenal set ["items", []];
|
||||
_vArsenal set ["weapons", []];
|
||||
_vArsenal set ["magazines", []];
|
||||
_vArsenal set ["backpacks", []];
|
||||
|
||||
_self set ["vArsenal", _vArsenal];
|
||||
}],
|
||||
["init", {
|
||||
private _uid = _self get "uid";
|
||||
private _vArsenal = _self get "vArsenal";
|
||||
|
||||
[SRPC(locker,requestInitVA), [_uid, _vArsenal]] call CFUNC(serverEvent);
|
||||
FORGE_Locker_Box = "ReammoBox_F" createVehicleLocal [0, 0, -999];
|
||||
|
||||
systemChat format ["VArsenal loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:VArsenal] VArsenal Class Initialized!";
|
||||
}],
|
||||
["save", {
|
||||
params [["_sync", false, [false]]];
|
||||
|
||||
private _uid = _self get "uid";
|
||||
[SRPC(locker,requestSaveVA), [_uid, _sync]] call CFUNC(serverEvent);
|
||||
|
||||
_self set ["lastSave", time];
|
||||
}],
|
||||
["sync", {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
|
||||
|
||||
private _vArsenal = _self get "vArsenal";
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
|
||||
if (_data isEqualTo createHashMap) exitWith { diag_log "[FORGE:Client:VArsenal] Empty data received for sync, skipping."; };
|
||||
|
||||
{
|
||||
_vArsenal set [_x, _y];
|
||||
|
||||
if (_jip) then {
|
||||
switch (_x) do {
|
||||
case "items": { _self call ["applyItems", []]; };
|
||||
case "weapons": { _self call ["applyWeapons", []]; };
|
||||
case "magazines": { _self call ["applyMagazines", []]; };
|
||||
case "backpacks": { _self call ["applyBackpacks", []]; };
|
||||
default {};
|
||||
};
|
||||
};
|
||||
} forEach _data;
|
||||
|
||||
_self set ["vArsenal", _vArsenal];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:VArsenal] Sync completed";
|
||||
}],
|
||||
["get", {
|
||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
||||
|
||||
private _vArsenal = _self get "vArsenal";
|
||||
_vArsenal getOrDefault [_key, _default];
|
||||
}],
|
||||
["applyItems", {
|
||||
private _items = _self call ["get", ["items", []]];
|
||||
[FORGE_Armory_Box, _items, false, true, 1, 0] call BFUNC(addVirtualItemCargo);
|
||||
}],
|
||||
["applyWeapons", {
|
||||
private _weapons = _self call ["get", ["weapons", []]];
|
||||
[FORGE_Armory_Box, _weapons, false, true, 1, 1] call BFUNC(addVirtualItemCargo);
|
||||
}],
|
||||
["applyMagazines", {
|
||||
private _magazines = _self call ["get", ["magazines", []]];
|
||||
[FORGE_Armory_Box, _magazines, false, true, 1, 2] call BFUNC(addVirtualItemCargo);
|
||||
}],
|
||||
["applyBackpacks", {
|
||||
private _backpacks = _self call ["get", ["backpacks", []]];
|
||||
[FORGE_Armory_Box, _backpacks, false, true, 1, 3] call BFUNC(addVirtualItemCargo);
|
||||
}]
|
||||
]];
|
||||
|
||||
SETVAR(player,FORGE_VArsenalClass,GVAR(VArsenalClass));
|
||||
GVAR(VArsenalClass)
|
||||
17
arma/client/addons/locker/functions/fnc_openUI.sqf
Normal file
17
arma/client/addons/locker/functions/fnc_openUI.sqf
Normal file
@ -0,0 +1,17 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Opens the locker UI.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_locker_fnc_openUI;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
9
arma/client/addons/locker/script_component.hpp
Normal file
9
arma/client/addons/locker/script_component.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#define COMPONENT locker
|
||||
#define COMPONENT_BEAUTIFIED Locker
|
||||
#include "\forge\forge_client\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#include "\forge\forge_client\addons\main\script_macros.hpp"
|
||||
8
arma/client/addons/locker/stringtable.xml
Normal file
8
arma/client/addons/locker/stringtable.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project name="FFE">
|
||||
<Package name="Locker">
|
||||
<Key ID="STR_forge_client_locker_displayName">
|
||||
<English>Locker</English>
|
||||
</Key>
|
||||
</Package>
|
||||
</Project>
|
||||
@ -14,6 +14,10 @@
|
||||
// Remote Procedure Calls
|
||||
#define CRPC(var1,var2) QUOTE(DOUBLES(DOUBLES(forge_client,var1),var2))
|
||||
#define SRPC(var1,var2) QUOTE(DOUBLES(DOUBLES(forge_server,var1),var2))
|
||||
#define SREG(var1,var2) DOUBLES(DOUBLES(forge_server,var1),var2)
|
||||
|
||||
#define CLASS(var1) DOUBLES(PREFIX,var1)
|
||||
#define QCLASS(var1) QUOTE(DOUBLES(PREFIX,var1))
|
||||
|
||||
#define QQUOTE(var1) QUOTE(QUOTE(var1))
|
||||
|
||||
|
||||
1
arma/client/addons/notifications/$PBOPREFIX$
Normal file
1
arma/client/addons/notifications/$PBOPREFIX$
Normal file
@ -0,0 +1 @@
|
||||
forge\forge_client\addons\notifications
|
||||
19
arma/client/addons/notifications/CfgEventHandlers.hpp
Normal file
19
arma/client/addons/notifications/CfgEventHandlers.hpp
Normal file
@ -0,0 +1,19 @@
|
||||
class Extended_PreStart_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PreInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
||||
};
|
||||
};
|
||||
4
arma/client/addons/notifications/README.md
Normal file
4
arma/client/addons/notifications/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
forge_client_notifications
|
||||
===================
|
||||
|
||||
Description for this addon
|
||||
3
arma/client/addons/notifications/XEH_PREP.hpp
Normal file
3
arma/client/addons/notifications/XEH_PREP.hpp
Normal file
@ -0,0 +1,3 @@
|
||||
PREP(handleUIEvents);
|
||||
PREP(initNotificationClass);
|
||||
PREP(openUI);
|
||||
1
arma/client/addons/notifications/XEH_postInit.sqf
Normal file
1
arma/client/addons/notifications/XEH_postInit.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
15
arma/client/addons/notifications/XEH_postInitClient.sqf
Normal file
15
arma/client/addons/notifications/XEH_postInitClient.sqf
Normal file
@ -0,0 +1,15 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
[{
|
||||
EGVAR(actor,ActorClass) get "isLoaded";
|
||||
}, {
|
||||
("NotificationHudLayer" call BFUNC(rscLayer)) cutRsc ["RscNotifications", "PLAIN"];
|
||||
[] call FUNC(openUI);
|
||||
if (isNil QGVAR(NotificationClass)) then { [] call FUNC(initNotificationClass); };
|
||||
}] call CFUNC(waitUntilAndExecute);
|
||||
|
||||
[QGVAR(recieveNotification), {
|
||||
params [["_type", "", [""]], ["_title", "", [""]], ["_content", "", [""]], ["_duration", 4000, [4000]]];
|
||||
|
||||
GVAR(NotificationClass) call ["create", [_type, _title, _content, _duration]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
10
arma/client/addons/notifications/XEH_preInit.sqf
Normal file
10
arma/client/addons/notifications/XEH_preInit.sqf
Normal file
@ -0,0 +1,10 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
// #include "initSettings.inc.sqf"
|
||||
// #include "initKeybinds.inc.sqf"
|
||||
1
arma/client/addons/notifications/XEH_preInitClient.sqf
Normal file
1
arma/client/addons/notifications/XEH_preInitClient.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "script_component.hpp"
|
||||
3
arma/client/addons/notifications/XEH_preStart.sqf
Normal file
3
arma/client/addons/notifications/XEH_preStart.sqf
Normal file
@ -0,0 +1,3 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
21
arma/client/addons/notifications/config.cpp
Normal file
21
arma/client/addons/notifications/config.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
author = AUTHOR;
|
||||
authors[] = {"IDSolutions"};
|
||||
url = ECSTRING(main,url);
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_client_main"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
VERSION_CONFIG;
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
#include "ui\RscCommon.hpp"
|
||||
#include "ui\RscNotifications.hpp"
|
||||
@ -0,0 +1,34 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Handles UI events.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_notifications_fnc_handleUIEvents;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
private _alert = fromJSON _message;
|
||||
private _event = _alert get "event";
|
||||
private _data = _alert get "data";
|
||||
|
||||
diag_log format ["[FORGE:Client:Notifications] Handling UI event: %1 with data: %2", _event, _data];
|
||||
|
||||
switch (_event) do {
|
||||
case "notifications::ready": {
|
||||
GVAR(NotificationClass) call ["init", []];
|
||||
};
|
||||
default { hint format ["[FORGE:Client:Notifications] Unhandled event: %1", _event]; };
|
||||
};
|
||||
|
||||
true;
|
||||
@ -0,0 +1,54 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Initialize notification class
|
||||
*
|
||||
* Arguments:
|
||||
* N/A
|
||||
*
|
||||
* Return Value:
|
||||
* N/A
|
||||
*
|
||||
* Examples:
|
||||
* [] call forge_client_notifications_fnc_initNotificationClass
|
||||
*
|
||||
* Public: Yes
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(NotificationClass) = createHashMapObject [[
|
||||
["#type", "INotificationClass"],
|
||||
["#create", {
|
||||
private _display = uiNamespace getVariable ["RscNotifications", nil];
|
||||
private _control = _display displayCtrl 1004;
|
||||
|
||||
_self set ["control", _control];
|
||||
_self set ["isLoaded", false];
|
||||
}],
|
||||
["init", {
|
||||
private _params = ["success", "System Ready", "Notification system handshake complete!", 3000];
|
||||
|
||||
_self call ["create", _params];
|
||||
_self set ["isLoaded", true];
|
||||
|
||||
systemChat format ["Notifications loaded for %1", (name player)];
|
||||
diag_log "[FORGE:Client:Notifications] Notification Class Initialized!";
|
||||
}],
|
||||
["create", {
|
||||
params [["_type", "", ["info"]], ["_title", "", [""]], ["_content", "", [""]], ["_duration", 4000]];
|
||||
|
||||
private _control = _self get "control";
|
||||
private _message = createHashMap;
|
||||
|
||||
_message set ["type", _type];
|
||||
_message set ["title", _title];
|
||||
_message set ["message", _content];
|
||||
_message set ["duration", _duration];
|
||||
|
||||
_control ctrlWebBrowserAction ["ExecJS", format ["window.dispatchEvent(new CustomEvent('forge:notify', { detail: %1 }))", (toJSON _message)]];
|
||||
}]
|
||||
]];
|
||||
|
||||
SETVAR(player,FORGE_NotificationClass,GVAR(NotificationClass));
|
||||
GVAR(NotificationClass)
|
||||
31
arma/client/addons/notifications/functions/fnc_openUI.sqf
Normal file
31
arma/client/addons/notifications/functions/fnc_openUI.sqf
Normal file
@ -0,0 +1,31 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* Author: IDSolutions
|
||||
* Open notification interface.
|
||||
*
|
||||
* Arguments:
|
||||
* None
|
||||
*
|
||||
* Return Value:
|
||||
* None
|
||||
*
|
||||
* Example:
|
||||
* [] call forge_client_notifications_fnc_openUI;
|
||||
*
|
||||
* Public: No
|
||||
*/
|
||||
|
||||
private _display = uiNamespace getVariable ["RscNotifications", nil];
|
||||
private _ctrl = (_display displayCtrl 1004);
|
||||
|
||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
||||
params ["_control", "_isConfirmDialog", "_message"];
|
||||
|
||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
||||
}];
|
||||
|
||||
_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
|
||||
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
|
||||
|
||||
true;
|
||||
1
arma/client/addons/notifications/initKeybinds.inc.sqf
Normal file
1
arma/client/addons/notifications/initKeybinds.inc.sqf
Normal file
@ -0,0 +1 @@
|
||||
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"
|
||||
1
arma/client/addons/notifications/initSettings.inc.sqf
Normal file
1
arma/client/addons/notifications/initSettings.inc.sqf
Normal file
@ -0,0 +1 @@
|
||||
// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required
|
||||
9
arma/client/addons/notifications/script_component.hpp
Normal file
9
arma/client/addons/notifications/script_component.hpp
Normal file
@ -0,0 +1,9 @@
|
||||
#define COMPONENT notifications
|
||||
#define COMPONENT_BEAUTIFIED Notifications
|
||||
#include "\forge\forge_client\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#include "\forge\forge_client\addons\main\script_macros.hpp"
|
||||
8
arma/client/addons/notifications/stringtable.xml
Normal file
8
arma/client/addons/notifications/stringtable.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project name="FFE">
|
||||
<Package name="Notifications">
|
||||
<Key ID="STR_forge_client_notifications_displayName">
|
||||
<English>Notifications</English>
|
||||
</Key>
|
||||
</Package>
|
||||
</Project>
|
||||
98
arma/client/addons/notifications/ui/RscCommon.hpp
Normal file
98
arma/client/addons/notifications/ui/RscCommon.hpp
Normal file
@ -0,0 +1,98 @@
|
||||
// Control types
|
||||
#define CT_STATIC 0
|
||||
#define CT_BUTTON 1
|
||||
#define CT_EDIT 2
|
||||
#define CT_SLIDER 3
|
||||
#define CT_COMBO 4
|
||||
#define CT_LISTBOX 5
|
||||
#define CT_TOOLBOX 6
|
||||
#define CT_CHECKBOXES 7
|
||||
#define CT_PROGRESS 8
|
||||
#define CT_HTML 9
|
||||
#define CT_STATIC_SKEW 10
|
||||
#define CT_ACTIVETEXT 11
|
||||
#define CT_TREE 12
|
||||
#define CT_STRUCTURED_TEXT 13
|
||||
#define CT_CONTEXT_MENU 14
|
||||
#define CT_CONTROLS_GROUP 15
|
||||
#define CT_SHORTCUTBUTTON 16
|
||||
#define CT_HITZONES 17
|
||||
#define CT_XKEYDESC 40
|
||||
#define CT_XBUTTON 41
|
||||
#define CT_XLISTBOX 42
|
||||
#define CT_XSLIDER 43
|
||||
#define CT_XCOMBO 44
|
||||
#define CT_ANIMATED_TEXTURE 45
|
||||
#define CT_OBJECT 80
|
||||
#define CT_OBJECT_ZOOM 81
|
||||
#define CT_OBJECT_CONTAINER 82
|
||||
#define CT_OBJECT_CONT_ANIM 83
|
||||
#define CT_LINEBREAK 98
|
||||
#define CT_USER 99
|
||||
#define CT_MAP 100
|
||||
#define CT_MAP_MAIN 101
|
||||
#define CT_LISTNBOX 102
|
||||
#define CT_ITEMSLOT 103
|
||||
#define CT_CHECKBOX 77
|
||||
|
||||
// Static styles
|
||||
#define ST_POS 0x0F
|
||||
#define ST_HPOS 0x03
|
||||
#define ST_VPOS 0x0C
|
||||
#define ST_LEFT 0x00
|
||||
#define ST_RIGHT 0x01
|
||||
#define ST_CENTER 0x02
|
||||
#define ST_DOWN 0x04
|
||||
#define ST_UP 0x08
|
||||
#define ST_VCENTER 0x0C
|
||||
|
||||
#define ST_TYPE 0xF0
|
||||
#define ST_SINGLE 0x00
|
||||
#define ST_MULTI 0x10
|
||||
#define ST_TITLE_BAR 0x20
|
||||
#define ST_PICTURE 0x30
|
||||
#define ST_FRAME 0x40
|
||||
#define ST_BACKGROUND 0x50
|
||||
#define ST_GROUP_BOX 0x60
|
||||
#define ST_GROUP_BOX2 0x70
|
||||
#define ST_HUD_BACKGROUND 0x80
|
||||
#define ST_TILE_PICTURE 0x90
|
||||
#define ST_WITH_RECT 0xA0
|
||||
#define ST_LINE 0xB0
|
||||
#define ST_UPPERCASE 0xC0
|
||||
#define ST_LOWERCASE 0xD0
|
||||
|
||||
#define ST_SHADOW 0x100
|
||||
#define ST_NO_RECT 0x200
|
||||
#define ST_KEEP_ASPECT_RATIO 0x800
|
||||
|
||||
// Slider styles
|
||||
#define SL_DIR 0x400
|
||||
#define SL_VERT 0
|
||||
#define SL_HORZ 0x400
|
||||
|
||||
#define SL_TEXTURES 0x10
|
||||
|
||||
// progress bar
|
||||
#define ST_VERTICAL 0x01
|
||||
#define ST_HORIZONTAL 0
|
||||
|
||||
// Listbox styles
|
||||
#define LB_TEXTURES 0x10
|
||||
#define LB_MULTI 0x20
|
||||
|
||||
// Tree styles
|
||||
#define TR_SHOWROOT 1
|
||||
#define TR_AUTOCOLLAPSE 2
|
||||
|
||||
// Default text sizes
|
||||
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
|
||||
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
|
||||
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
|
||||
|
||||
// Pixel grid
|
||||
#define pixelScale 0.50
|
||||
#define GRID_W (pixelW * pixelGrid * pixelScale)
|
||||
#define GRID_H (pixelH * pixelGrid * pixelScale)
|
||||
|
||||
class RscText;
|
||||
22
arma/client/addons/notifications/ui/RscNotifications.hpp
Normal file
22
arma/client/addons/notifications/ui/RscNotifications.hpp
Normal file
@ -0,0 +1,22 @@
|
||||
class RscTitles {
|
||||
class RscNotifications {
|
||||
idd = 1003;
|
||||
fadein = 0;
|
||||
fadeout = 0;
|
||||
duration = 1e+011;
|
||||
onLoad = "uinamespace setVariable ['RscNotifications', _this select 0]";
|
||||
onUnLoad = "uinamespace setVariable ['RscNotifications', nil]";
|
||||
|
||||
class controlsBackground {};
|
||||
class controls {
|
||||
class IFrame: RscText {
|
||||
type = 106;
|
||||
idc = 1004;
|
||||
x = "safeZoneX";
|
||||
y = "safeZoneY";
|
||||
w = "safeZoneW";
|
||||
h = "safeZoneH";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
38
arma/client/addons/notifications/ui/_site/index.html
Normal file
38
arma/client/addons/notifications/ui/_site/index.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forge - Notification System</title>
|
||||
<!--
|
||||
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("forge\\forge_client\\addons\\notifications\\ui\\_site\\styles.css"),
|
||||
// Load JavaScript file (now using Redux-like pattern)
|
||||
A3API.RequestFile("forge\\forge_client\\addons\\notifications\\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);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Main notification container -->
|
||||
<div id="notification-container" class="notification-container" role="region" aria-label="Notifications"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
263
arma/client/addons/notifications/ui/_site/script.js
Normal file
263
arma/client/addons/notifications/ui/_site/script.js
Normal file
@ -0,0 +1,263 @@
|
||||
//=============================================================================
|
||||
// #region ACTIONS
|
||||
//=============================================================================
|
||||
|
||||
const NotificationActionTypes = {
|
||||
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
|
||||
REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION',
|
||||
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
||||
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION'
|
||||
};
|
||||
|
||||
const notificationActions = {
|
||||
addNotification: (notification) => ({
|
||||
type: NotificationActionTypes.ADD_NOTIFICATION,
|
||||
payload: {
|
||||
id: Date.now() + Math.random(),
|
||||
timestamp: Date.now(),
|
||||
type: 'info',
|
||||
title: 'Notification',
|
||||
message: 'Default message',
|
||||
duration: 0,
|
||||
status: 'showing',
|
||||
...notification
|
||||
}
|
||||
}),
|
||||
removeNotification: (id) => ({
|
||||
type: NotificationActionTypes.REMOVE_NOTIFICATION,
|
||||
payload: { id }
|
||||
}),
|
||||
clearNotifications: () => ({
|
||||
type: NotificationActionTypes.CLEAR_NOTIFICATIONS
|
||||
}),
|
||||
updateNotification: (id, updates) => ({
|
||||
type: NotificationActionTypes.UPDATE_NOTIFICATION,
|
||||
payload: { id, updates }
|
||||
})
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// #region REDUCER
|
||||
//=============================================================================
|
||||
|
||||
const notificationInitialState = {
|
||||
notifications: [],
|
||||
maxNotifications: 5
|
||||
};
|
||||
|
||||
function notificationReducer(state = notificationInitialState, action = {}) {
|
||||
switch (action.type) {
|
||||
case NotificationActionTypes.ADD_NOTIFICATION: {
|
||||
if (!action.payload) return state;
|
||||
let newNotifications = [...state.notifications];
|
||||
if (newNotifications.length >= state.maxNotifications) {
|
||||
newNotifications = newNotifications.slice(1);
|
||||
}
|
||||
return { ...state, notifications: [...newNotifications, action.payload] };
|
||||
}
|
||||
case NotificationActionTypes.REMOVE_NOTIFICATION: {
|
||||
if (!action.payload || !action.payload.id) return state;
|
||||
return {
|
||||
...state,
|
||||
notifications: state.notifications.filter(n => n.id !== action.payload.id)
|
||||
};
|
||||
}
|
||||
case NotificationActionTypes.CLEAR_NOTIFICATIONS:
|
||||
return { ...state, notifications: [] };
|
||||
case NotificationActionTypes.UPDATE_NOTIFICATION: {
|
||||
if (!action.payload || !action.payload.id || !action.payload.updates) return state;
|
||||
return {
|
||||
...state,
|
||||
notifications: state.notifications.map(n =>
|
||||
n.id === action.payload.id ? { ...n, ...action.payload.updates } : n
|
||||
)
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region STORE
|
||||
//=============================================================================
|
||||
|
||||
class Store {
|
||||
constructor(reducer, initialState) {
|
||||
this.reducer = reducer;
|
||||
this.state = initialState;
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
dispatch(action) {
|
||||
this.state = this.reducer(this.state, action);
|
||||
this.listeners.forEach(listener => listener(this.state));
|
||||
return action;
|
||||
}
|
||||
|
||||
subscribe(listener) {
|
||||
this.listeners.push(listener);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== listener);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const notificationStore = new Store(notificationReducer, notificationInitialState);
|
||||
|
||||
//=============================================================================
|
||||
// #region SELECTORS
|
||||
//=============================================================================
|
||||
|
||||
const notificationSelectors = {
|
||||
getNotifications: (state) => state.notifications,
|
||||
getMaxNotifications: (state) => state.maxNotifications
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// #region UI COMPONENT
|
||||
//=============================================================================
|
||||
|
||||
class NotificationUI {
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
this.unsubscribe = null;
|
||||
this.container = document.getElementById('notification-container');
|
||||
this.renderedNotifications = new Map();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (!this.container) {
|
||||
console.error('Notification container not found');
|
||||
return;
|
||||
}
|
||||
this.unsubscribe = this.store.subscribe((state) => this.render(state));
|
||||
this.render(this.store.getState());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.unsubscribe) this.unsubscribe();
|
||||
this.renderedNotifications.forEach(el => {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
this.renderedNotifications.clear();
|
||||
}
|
||||
|
||||
render(state) {
|
||||
const notifications = notificationSelectors.getNotifications(state);
|
||||
|
||||
// Remove notifications no longer present
|
||||
const currentIds = new Set(notifications.map(n => n.id));
|
||||
for (const [id, el] of this.renderedNotifications.entries()) {
|
||||
if (!currentIds.has(id)) {
|
||||
if (el.parentNode) el.parentNode.removeChild(el);
|
||||
this.renderedNotifications.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update notifications
|
||||
notifications.forEach(notification => {
|
||||
if (!notification || !notification.id) return;
|
||||
if (!this.renderedNotifications.has(notification.id)) {
|
||||
this.createNotificationElement(notification);
|
||||
} else {
|
||||
this.updateNotificationElement(notification);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNotificationElement(notification) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `notification ${notification.type || 'info'}`;
|
||||
el.dataset.id = notification.id;
|
||||
el.innerHTML = `
|
||||
<div class="notification-header">
|
||||
<div class="notification-title">${notification.title || 'Notification'}</div>
|
||||
</div>
|
||||
<div class="notification-message">${notification.message || 'No message'}</div>
|
||||
${notification.duration > 0 ? '<div class="notification-progress"><div class="notification-progress-bar"></div></div>' : ''}
|
||||
`;
|
||||
this.container.appendChild(el);
|
||||
this.renderedNotifications.set(notification.id, el);
|
||||
setTimeout(() => el.classList.add('show'), 10);
|
||||
|
||||
// Set progress bar animation duration
|
||||
if (notification.duration > 0) {
|
||||
const progressBar = el.querySelector('.notification-progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.transition = `width ${notification.duration}ms linear`;
|
||||
progressBar.style.width = '100%';
|
||||
setTimeout(() => {
|
||||
progressBar.style.width = '0%';
|
||||
}, 20);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
notificationStore.dispatch(notificationActions.updateNotification(notification.id, { status: 'hiding' }));
|
||||
setTimeout(() => {
|
||||
notificationStore.dispatch(notificationActions.removeNotification(notification.id));
|
||||
}, 300);
|
||||
}, notification.duration);
|
||||
}
|
||||
}
|
||||
|
||||
updateNotificationElement(notification) {
|
||||
const el = this.renderedNotifications.get(notification.id);
|
||||
if (!el) return;
|
||||
if (notification.status === 'hiding') el.classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// #region GLOBAL API & EVENT HANDLING
|
||||
//=============================================================================
|
||||
|
||||
let notificationUI = null;
|
||||
let notificationUIInitialized = false;
|
||||
|
||||
function notifyArmaNotificationReady() {
|
||||
if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") {
|
||||
window.parent.postMessage({ event: "notifications::ready" }, "*");
|
||||
}
|
||||
if (typeof A3API !== "undefined" && typeof A3API.SendAlert === "function") {
|
||||
A3API.SendAlert(JSON.stringify({ event: "notifications::ready" }));
|
||||
}
|
||||
}
|
||||
|
||||
function initializeNotifications() {
|
||||
if (notificationUIInitialized) {
|
||||
console.log('Notification system already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
notificationUI = new NotificationUI(notificationStore);
|
||||
notificationUI.init();
|
||||
notificationUIInitialized = true;
|
||||
console.log('Notification system is ready!');
|
||||
notifyArmaNotificationReady();
|
||||
}
|
||||
|
||||
// Expose global notification API
|
||||
const showNotification = (type, title, message, duration) => {
|
||||
return notificationStore.dispatch(notificationActions.addNotification({ type, title, message, duration }));
|
||||
};
|
||||
const clearAllNotifications = () => {
|
||||
return notificationStore.dispatch(notificationActions.clearNotifications());
|
||||
};
|
||||
|
||||
// Listen for global notification events (for Arma/SQF or other scripts)
|
||||
window.addEventListener('forge:notify', function (e) {
|
||||
if (!e || !e.detail) return;
|
||||
const { type, title, message, duration } = e.detail;
|
||||
showNotification(type, title, message, duration);
|
||||
});
|
||||
|
||||
// Auto-initialize if DOM is already loaded when script executes
|
||||
if (document.readyState !== 'loading') {
|
||||
initializeNotifications();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', initializeNotifications, { once: true });
|
||||
}
|
||||
174
arma/client/addons/notifications/ui/_site/styles.css
Normal file
174
arma/client/addons/notifications/ui/_site/styles.css
Normal file
@ -0,0 +1,174 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: transparent;
|
||||
min-height: 100vh;
|
||||
color: rgba(200, 220, 240, 0.95);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Notification Container */
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 120px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
width: 350px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Individual Notification */
|
||||
.notification {
|
||||
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;
|
||||
box-shadow:
|
||||
0 0 20px rgba(100, 150, 200, 0.15),
|
||||
0 4px 16px rgba(0, 0, 0, 0.8);
|
||||
margin-bottom: 10px;
|
||||
padding: 1rem 1.25rem;
|
||||
width: 100%;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.15s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&.hide {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Notification Types */
|
||||
&.success {
|
||||
border-left-color: rgba(100, 200, 150, 0.6);
|
||||
|
||||
.notification-title {
|
||||
color: rgba(150, 255, 200, 0.9);
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(100, 200, 150, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border-left-color: rgba(220, 100, 100, 0.6);
|
||||
|
||||
.notification-title {
|
||||
color: rgba(255, 150, 150, 0.9);
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(220, 100, 100, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-left-color: rgba(200, 150, 100, 0.6);
|
||||
|
||||
.notification-title {
|
||||
color: rgba(255, 200, 150, 0.9);
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(200, 150, 100, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
border-left-color: rgba(100, 150, 200, 0.6);
|
||||
|
||||
.notification-title {
|
||||
color: rgba(150, 200, 255, 0.9);
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
background: rgba(100, 150, 200, 0.8);
|
||||
animation: progress 5s linear forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Notification Content */
|
||||
.notification-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
flex: 1;
|
||||
color: rgba(200, 220, 255, 1);
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
color: rgba(140, 160, 180, 0.9);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Progress bar for auto-dismiss */
|
||||
.notification-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: rgba(15, 20, 30, 0.5);
|
||||
width: 100%;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.notification-progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transform-origin: left;
|
||||
border-radius: 0 0 4px 4px;
|
||||
transition: width linear;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.notification-container {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.notification {
|
||||
transform: translateY(-100%);
|
||||
|
||||
&.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.hide {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.notification-container {
|
||||
width: calc(100vw - 40px);
|
||||
}
|
||||
}
|
||||
@ -10,9 +10,6 @@ if (isNil QGVAR(OrgClass)) then { [] call FUNC(initOrgClass); };
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(OrgClass) call ["sync", [_data, true]];
|
||||
|
||||
SETPVAR(player,FORGE_isLoaded,true);
|
||||
cutText ["", "PLAIN", 1];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseSyncOrg), {
|
||||
|
||||
@ -59,7 +59,6 @@ GVAR(OrgClass) = createHashMapObject [[
|
||||
private _isLoaded = _self get "isLoaded";
|
||||
private _org = _self get "org";
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
if (_data isEqualTo createHashMap) exitWith {
|
||||
diag_log "[FORGE:Client:Org] Empty data received for sync, skipping.";
|
||||
};
|
||||
@ -67,6 +66,8 @@ GVAR(OrgClass) = createHashMapObject [[
|
||||
{ _org set [_x, _y]; } forEach _data;
|
||||
|
||||
_self set ["org", _org];
|
||||
|
||||
if !(_isLoaded) then { _self set ["isLoaded", true]; };
|
||||
diag_log "[FORGE:Client:Org] Sync completed";
|
||||
}],
|
||||
["get", {
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
[["76561198027566824",[["rank","PRIVATE"],["earnings",0],["holster",true],["state","HEALTHY"],["bank",0],["organization","0160566824_org"],["transactions",[]],["loadout",[[],[],[],["U_BG_Guerrilla_6_1",[]],[],[],"H_Cap_blk_ION","",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]],["stance","CROUCH"],["cash",0],["uid","76561198027566824"],["phone_number","0160566824"],["direction",0],["position",[4000,4000,0]],["email","0160566824@spearnet.mil"]]]];
|
||||
|
||||
["{""uid"":""76561198027566824"",""loadout"":[[],[],[],[""U_BG_Guerrilla_6_1"",[]],[],[],""H_Cap_blk_ION"","""",[],[""ItemMap"",""ItemGPS"",""ItemRadio"",""ItemCompass"",""ItemWatch"",""""]],""position"":[4000.0,4000.0,0.0],""direction"":0.0,""stance"":""CROUCH"",""email"":""0160566824@spearnet.mil"",""phone_number"":""0160566824"",""bank"":0.0,""cash"":0.0,""earnings"":0.0,""state"":""HEALTHY"",""holster"":true,""rank"":""PRIVATE"",""organization"":""0160566824_org"",""transactions"":[]}",0,0];
|
||||
|
||||
["{""id"":""0160566824_org"",""owner"":""76561198027566824"",""name"":""Black Rifle Company"",""funds"":0.0,""reputation"":0}",0,0];
|
||||
@ -2,7 +2,7 @@
|
||||
options.banned = [
|
||||
"spawn", # Scheduled should be avoided whenever possible
|
||||
"execVM", # Script files should never be run directly, they should be functions
|
||||
"remoteExec", # CBA events should be used for networking
|
||||
# "remoteExec", # CBA events should be used for networking
|
||||
]
|
||||
|
||||
[sqf.banned_macros]
|
||||
|
||||
@ -11,10 +11,11 @@ git_hash = 0
|
||||
include = [
|
||||
"mod.cpp",
|
||||
"meta.cpp",
|
||||
"logo_forge_server.png",
|
||||
"logo_forge_server_over.png",
|
||||
"logo_forge_server_ca.paa",
|
||||
"logo_forge_server_over_ca.paa",
|
||||
"icon_64_ca.paa",
|
||||
"icon_128_ca.paa",
|
||||
"icon_128_highlight_ca.paa",
|
||||
"title_ca.paa",
|
||||
"config.example.toml",
|
||||
"LICENSE.md",
|
||||
"README.md",
|
||||
]
|
||||
|
||||
@ -6,8 +6,6 @@ PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
if (isNil QGVAR(ActorStore)) then { [] call FUNC(initActorStore); };
|
||||
|
||||
[QGVAR(requestInitActor), {
|
||||
params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]];
|
||||
|
||||
@ -69,17 +67,3 @@ if (isNil QGVAR(ActorStore)) then { [] call FUNC(initActorStore); };
|
||||
|
||||
GVAR(ActorStore) call ["remove", [_uid]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestResync), {
|
||||
params ["_player"];
|
||||
|
||||
private _uid = getPlayerUID _player;
|
||||
private _actor = GVAR(ActorStore) call ["get", [_uid, true]];
|
||||
|
||||
private _session = GVAR(PlayerSessions) getOrDefault [_uid, nil];
|
||||
if (isNil "_session") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid Session!" };
|
||||
|
||||
[CRPC(actor,responseSyncActor), [_actor, true], _player] call CFUNC(targetEvent);
|
||||
|
||||
diag_log format ["[FORGE:Server:Actor] Resync completed for %1", _uid];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -8,7 +8,8 @@ class CfgPatches {
|
||||
name = COMPONENT_NAME;
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
requiredAddons[] = {
|
||||
"forge_server_main"
|
||||
"forge_server_main",
|
||||
"forge_server_common"
|
||||
};
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
|
||||
@ -18,11 +18,19 @@
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(ActorStore) = createHashMapObject [[
|
||||
["#base", EGVAR(common,BaseStore)],
|
||||
["#type", "IActorStore"],
|
||||
["#create", {
|
||||
GVAR(ActorRegistry) = createHashMap;
|
||||
GVAR(PlayerSessions) = createHashMap;
|
||||
diag_log "[FORGE:Server:Actor] Actor Store Initialized!";
|
||||
|
||||
_self set ["_registry", GVAR(ActorRegistry)];
|
||||
_self set ["_extCallPrefix", "actor"];
|
||||
_self set ["_readMethod", "get"];
|
||||
_self set ["_storeName", "Actor"];
|
||||
_self set ["_syncEventName", CRPC(actor,responseSyncActor)];
|
||||
|
||||
["INFO", "Actor Store Initialized!", nil, nil] call EFUNC(common,log);
|
||||
}],
|
||||
["generateSessionToken", {
|
||||
params [["_uid", "", [""]]];
|
||||
@ -30,7 +38,9 @@ GVAR(ActorStore) = createHashMapObject [[
|
||||
private _token = format ["%1_%2_%3", _uid, floor(random 999999), time];
|
||||
private _sessionToken = _token call EFUNC(common,generateHash);
|
||||
|
||||
GVAR(PlayerSessions) set [_uid, _sessionToken];
|
||||
private _regEntry = createHashMapFromArray [["sessionToken", _sessionToken]];
|
||||
GVAR(PlayerSessions) set [_uid, _regEntry];
|
||||
|
||||
_sessionToken
|
||||
}],
|
||||
["init", {
|
||||
@ -39,20 +49,20 @@ GVAR(ActorStore) = createHashMapObject [[
|
||||
_self call ["generateSessionToken", [_uid]];
|
||||
private _finalActor = createHashMap;
|
||||
|
||||
EXTCALL("actor:exists",[ARR_1(_uid)]);
|
||||
private _exists = (_ext_res select 0) == "true";
|
||||
["actor:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
|
||||
private _exists = _result == "true";
|
||||
|
||||
if !(_exists) then {
|
||||
_finalActor = _defaultActor;
|
||||
_finalActor set ["uid", _uid];
|
||||
|
||||
private _json = _self call ["toJSON", [_finalActor]];
|
||||
EXTCALL("actor:create",[ARR_2(_uid,_json)]);
|
||||
["actor:create", [_uid, _json]] call EFUNC(extension,extCall);
|
||||
|
||||
private _phone_number = _finalActor getOrDefault ["phone_number", ""];
|
||||
private _email = _finalActor getOrDefault ["email", ""];
|
||||
|
||||
diag_log format ["[FORGE:Server:Actor] New player %1 registered with phone number: %2, email: %3", _uid, _phone_number, _email];
|
||||
["INFO", format ["New player %1 registered with phone number: %2, email: %3", _uid, _phone_number, _email], nil, nil] call EFUNC(common,log);
|
||||
} else {
|
||||
private _existingActor = _self call ["fetch", [_uid]];
|
||||
_finalActor = _existingActor;
|
||||
@ -68,112 +78,6 @@ GVAR(ActorStore) = createHashMapObject [[
|
||||
[CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent);
|
||||
|
||||
_finalActor
|
||||
}],
|
||||
["fetch", {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _actor = createHashMap;
|
||||
|
||||
EXTCALL("actor:get",[ARR_1(_uid)]);
|
||||
diag_log format ["[FORGE:Server:Actor] Data: %1", _ext_res];
|
||||
|
||||
private _ext_res_actor = _ext_res select 0;
|
||||
if (count _ext_res_actor > 0) then { _actor = _self call ["toHashMap", [_ext_res_actor]]; };
|
||||
|
||||
_actor
|
||||
}],
|
||||
["get", {
|
||||
params [["_uid", "", [""]], ["_sync", false, [false]]];
|
||||
|
||||
private _finalActor = createHashMap;
|
||||
|
||||
if (_sync) then {
|
||||
private _existingActor = _self call ["fetch", [_uid]];
|
||||
_finalActor = _existingActor;
|
||||
|
||||
GVAR(ActorRegistry) set [_uid, _finalActor];
|
||||
} else {
|
||||
_finalActor = GVAR(ActorRegistry) get _uid;
|
||||
};
|
||||
|
||||
_finalActor
|
||||
}],
|
||||
["set", {
|
||||
params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil], ["_sync", false, [false]]];
|
||||
|
||||
private _existingActor = GVAR(ActorRegistry) get _uid;
|
||||
private _finalActor = +_existingActor;
|
||||
private _hashMap = createHashMap;
|
||||
|
||||
_finalActor set [_field, _value];
|
||||
_hashMap set [_field, _value];
|
||||
|
||||
GVAR(ActorRegistry) set [_uid, _finalActor];
|
||||
|
||||
if (_sync) then {
|
||||
private _json = _self call ["toJSON", [_hashMap]];
|
||||
EXTCALL("actor:update",[ARR_2(_uid,_json)]);
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
[CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent);
|
||||
|
||||
_hashMap
|
||||
}],
|
||||
["mset", {
|
||||
params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
|
||||
|
||||
private _existingActor = GVAR(ActorRegistry) get _uid;
|
||||
private _finalActor = +_existingActor;
|
||||
private _hashMap = createHashMap;
|
||||
|
||||
{ _finalActor set [_x, _y]; } forEach _fieldValuePairs;
|
||||
{ _hashMap set [_x, _y]; } forEach _fieldValuePairs;
|
||||
|
||||
GVAR(ActorRegistry) set [_uid, _finalActor];
|
||||
|
||||
if (_sync) then {
|
||||
private _json = _self call ["toJSON", [_hashMap]];
|
||||
EXTCALL("actor:update",[ARR_2(_uid,_json)]);
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
[CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent);
|
||||
|
||||
_hashMap
|
||||
}],
|
||||
["save", {
|
||||
params [["_uid", "", [""]], ["_sync", false, [false]]];
|
||||
|
||||
private _existingActor = GVAR(ActorRegistry) get _uid;
|
||||
private _finalActor = +_existingActor;
|
||||
private _json = _self call ["toJSON", [_finalActor]];
|
||||
|
||||
EXTCALL("actor:update",[ARR_2(_uid,_json)]);
|
||||
|
||||
if (_sync) then {
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
[CRPC(actor,responseSyncActor), [_finalActor], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
_finalActor
|
||||
}],
|
||||
["remove", {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
GVAR(ActorRegistry) deleteAt _uid;
|
||||
}],
|
||||
["toHashMap", {
|
||||
params [["_data", "", [""]]];
|
||||
|
||||
private _hashMap = fromJSON _data;
|
||||
_hashMap
|
||||
}],
|
||||
["toJSON", {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _json = toJSON _data;
|
||||
_json
|
||||
}]
|
||||
]];
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user