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:
Jacob Schmidt 2026-01-04 12:52:15 -06:00
parent 7ce6c0bcad
commit ebfe77a340
230 changed files with 11804 additions and 1922 deletions

View File

@ -1,6 +1,7 @@
[workspace]
members = [
"arma/server/extension",
"bin/icom",
"lib/models",
"lib/repositories",
"lib/services",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
updateBalances();
}
// ============================================================================
// INITIALIZATION
// ============================================================================
// Initialize
function initATM() {
console.log('ATM interface initializing...');
// Subscribe to store updates
if (typeof store !== 'undefined') {
store.subscribe(() => {
updateBalances();
});
}
// 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;

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

View File

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

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

View File

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

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

View File

@ -17,3 +17,4 @@ class CfgPatches {
};
#include "CfgEventHandlers.hpp"
#include "CfgVehicles.hpp"

View File

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

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

View File

@ -0,0 +1,4 @@
forge_client_garage
===================
Description for this addon

View File

@ -0,0 +1,3 @@
PREP(initGarageClass);
PREP(initVGClass);
PREP(openVG);

View File

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

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

View 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) = [];

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

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

View File

@ -0,0 +1,4 @@
forge_client_locker
===================
Description for this addon

View File

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

View File

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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

View File

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

View File

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

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

View File

@ -0,0 +1,4 @@
forge_client_notifications
===================
Description for this addon

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
#include "script_component.hpp"
class CfgPatches {
class ADDON {
author = AUTHOR;
authors[] = {"IDSolutions"};
url = ECSTRING(main,url);
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
"forge_client_main"
};
units[] = {};
weapons[] = {};
VERSION_CONFIG;
};
};
#include "CfgEventHandlers.hpp"
#include "ui\RscCommon.hpp"
#include "ui\RscNotifications.hpp"

View File

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

View File

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

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

View File

@ -0,0 +1 @@
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"

View File

@ -0,0 +1 @@
// Can use localize "STR_ACE_Common_Enabled" for name if ACE is required

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,8 @@ class CfgPatches {
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
"forge_server_main"
"forge_server_main",
"forge_server_common"
};
units[] = {};
weapons[] = {};

View File

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