Jacob Schmidt ebfe77a340 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>
2026-01-04 12:52:15 -06:00

282 lines
9.9 KiB
JavaScript

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